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 6550f58da4..e6fa1ab632 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -116,6 +116,22 @@
"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)\",\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, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\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},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}"
}
+ },
+ {
+ "alias": "entities_hierarchy",
+ "name": "Entities hierarchy",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 3.5,
+ "resources": [],
+ "templateHtml": "\n ",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.hierarchyId = \"hierarchy-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-hierarchy-data-updated', self.ctx.$scope.hierarchyId);\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
+ "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\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\":{\"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\":{}}"
+ }
}
]
}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json
index a873c7a7e6..1f4807c7d4 100644
--- a/application/src/main/data/json/system/widget_bundles/maps.json
+++ b/application/src/main/data/json/system/widget_bundles/maps.json
@@ -116,6 +116,22 @@
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\"}]},{\"type\":\"function\",\"name\":\"Second Point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermomether\\\";\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"tmDefaultMapType\":\"roadmap\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipPattern\":\"
${entityName} Latitude: ${latitude:7}Longitude: ${longitude:7}Temperature: ${temperature} °CSee advanced settings for details
\",\"markerImageSize\":34,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['type'];\\nif (type == 'thermomether') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"labelFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt ';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C ';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['deviceType'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName} Energy: ${energy:2} kWt ';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName} Temperature: ${temperature:2} °C ';\\r\\n }\\r\\n}\"},\"title\":\"Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
+ },
+ {
+ "alias": "test",
+ "name": "Trip Animation",
+ "descriptor": {
+ "type": "timeseries",
+ "sizeX": 8.5,
+ "sizeY": 6.5,
+ "resources": [],
+ "templateHtml": " ",
+ "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
+ "controllerScript": " self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n }\n \n \n self.actionSources = function () {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n };\n",
+ "settingsSchema": "{\r\n \"schema\": {\r\n \"title\": \"Openstreet Map Configuration\",\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"mapProvider\": {\r\n \"title\": \"Map provider\",\r\n \"type\": \"string\",\r\n \"default\": \"OpenStreetMap.Mapnik\"\r\n },\r\n \"defaultZoomLevel\": {\r\n\t\t\t\t\t\"title\": \"Default map zoom level (1 - 20)\",\r\n\t\t\t\t\t\"type\": \"number\"\r\n\t\t\t\t},\r\n\t\t\t\"fitMapBounds\": {\r\n\t\t\t\t\"title\": \"Fit map bounds to cover all markers\",\r\n\t\t\t\t\"type\": \"boolean\",\r\n\t\t\t\t\"default\": true\r\n\t\t\t},\r\n \"latKeyName\": {\r\n \"title\": \"Latitude key name\",\r\n \"type\": \"string\",\r\n \"default\": \"latitude\"\r\n },\r\n \"lngKeyName\": {\r\n \"title\": \"Longitude key name\",\r\n \"type\": \"string\",\r\n \"default\": \"longitude\"\r\n },\r\n \"showLabel\": {\r\n \"title\": \"Show label\",\r\n \"type\": \"boolean\",\r\n \"default\": true\r\n },\r\n \"label\": {\r\n \"title\": \"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )\",\r\n \"type\": \"string\",\r\n \"default\": \"${entityName}\"\r\n },\r\n \"useLabelFunction\": {\r\n \"title\": \"Use label function\",\r\n \"type\": \"boolean\",\r\n \"default\": false\r\n },\r\n \"labelFunction\": {\r\n \"title\": \"Label function: f(data, dsData, dsIndex)\",\r\n \"type\": \"string\"\r\n },\r\n \"showTooltip\": {\r\n \"title\": \"Show tooltip\",\r\n \"type\": \"boolean\",\r\n \"default\": true\r\n },\r\n \"tooltipColor\": {\r\n \"title\": \"Tooltip background color\",\r\n \"type\": \"string\",\r\n \"default\": \"#fff\"\r\n },\r\n \"tooltipFontColor\": {\r\n \"title\": \"Tooltip font color\",\r\n \"type\": \"string\",\r\n \"default\": \"#000\"\r\n },\r\n \"tooltipOpacity\": {\r\n \"title\": \"Tooltip opacity (0-1)\",\r\n \"type\": \"number\",\r\n \"default\": 1 \r\n },\r\n \"tooltipPattern\": {\r\n \"title\": \"Tooltip (for ex. 'Text ${keyName} units.' or Link text ')\",\r\n \"type\": \"string\",\r\n \"default\": \"${entityName} Latitude: ${latitude:7}Longitude: ${longitude:7}End Time: ${maxTime}Start Time: ${minTime}\"\r\n },\r\n \"useTooltipFunction\": {\r\n \"title\": \"Use tooltip function\",\r\n \"type\": \"boolean\",\r\n \"default\": false\r\n },\r\n \"tooltipFunction\": {\r\n \"title\": \"Tooltip function: f(data, dsData, dsIndex)\",\r\n \"type\": \"string\"\r\n },\r\n \"color\": {\r\n \"title\": \"Stroke color\",\r\n \"type\": \"string\"\r\n },\r\n \"strokeWeight\": {\r\n \"title\": \"Stroke weight\",\r\n \"type\": \"number\",\r\n \"default\": 2\r\n },\r\n \"strokeOpacity\": {\r\n \"title\": \"Stroke opacity\",\r\n \"type\": \"number\",\r\n \"default\": 1\r\n },\r\n \"useColorFunction\": {\r\n \"title\": \"Use stroke color function\",\r\n \"type\": \"boolean\",\r\n \"default\": false\r\n },\r\n \"colorFunction\": {\r\n \"title\": \"Stroke color function: f(data, dsData, dsIndex)\",\r\n \"type\": \"string\"\r\n },\r\n \"showPoints\": {\r\n \"title\": \"Show points\",\r\n \"type\": \"boolean\",\r\n \"default\": false\r\n },\r\n \"pointColor\": {\r\n \"title\": \"Point color\",\r\n \"type\": \"string\"\r\n },\r\n \"pointSize\": {\r\n \"title\": \"Point size (px)\",\r\n \"type\": \"number\",\r\n \"default\": 10\r\n },\r\n \"defaultMarkerColor\": {\r\n \"title\": \"color for default marker\",\r\n \"type\": \"string\"\r\n },\r\n \"markerImage\": {\r\n \"title\": \"Custom marker image\",\r\n \"type\": \"string\"\r\n },\r\n \"markerImageSize\": {\r\n \"title\": \"Custom marker image size (px)\",\r\n \"type\": \"number\",\r\n \"default\": 34\r\n },\r\n \"rotationAngle\": {\r\n \"title\": \"Set additional rotation angle for marker (deg)\",\r\n \"type\": \"number\",\r\n \"default\": 180\r\n },\r\n \"useMarkerImageFunction\":{\r\n \"title\":\"Use marker image function\",\r\n \"type\":\"boolean\",\r\n \"default\":false\r\n },\r\n \"markerImageFunction\":{\r\n \"title\":\"Marker image function: f(data, images, dsData, dsIndex)\",\r\n \"type\":\"string\"\r\n },\r\n \"markerImages\":{\r\n \"title\":\"Marker images\",\r\n \"type\":\"array\",\r\n \"items\":{\r\n \"title\":\"Marker image\",\r\n \"type\":\"string\"\r\n }\r\n }\r\n },\r\n \"required\": []\r\n },\r\n \"form\": [{\r\n \"key\": \"mapProvider\",\r\n \"type\": \"rc-select\",\r\n \"multiple\": false,\r\n \"items\": [{\r\n \"value\": \"OpenStreetMap.Mapnik\",\r\n \"label\": \"OpenStreetMap.Mapnik (Default)\"\r\n }, {\r\n \"value\": \"OpenStreetMap.BlackAndWhite\",\r\n \"label\": \"OpenStreetMap.BlackAndWhite\"\r\n }, {\r\n \"value\": \"OpenStreetMap.HOT\",\r\n \"label\": \"OpenStreetMap.HOT\"\r\n }, {\r\n \"value\": \"Esri.WorldStreetMap\",\r\n \"label\": \"Esri.WorldStreetMap\"\r\n }, {\r\n \"value\": \"Esri.WorldTopoMap\",\r\n \"label\": \"Esri.WorldTopoMap\"\r\n }, {\r\n \"value\": \"CartoDB.Positron\",\r\n \"label\": \"CartoDB.Positron\"\r\n }, {\r\n \"value\": \"CartoDB.DarkMatter\",\r\n \"label\": \"CartoDB.DarkMatter\"\r\n }]\r\n },\"defaultZoomLevel\", \"fitMapBounds\", \"latKeyName\", \"lngKeyName\", \"showLabel\", \"label\", \"useLabelFunction\", {\r\n \"key\": \"labelFunction\",\r\n \"type\": \"javascript\"\r\n }, \"showTooltip\", {\r\n \"key\": \"tooltipColor\",\r\n \"type\": \"color\"\r\n }, {\r\n \"key\": \"tooltipFontColor\",\r\n \"type\": \"color\"\r\n },\"tooltipOpacity\", {\r\n \"key\": \"tooltipPattern\",\r\n \"type\": \"textarea\"\r\n }, \"useTooltipFunction\", {\r\n \"key\": \"tooltipFunction\",\r\n \"type\": \"javascript\"\r\n }, {\r\n \"key\": \"color\",\r\n \"type\": \"color\"\r\n }, \"useColorFunction\", {\r\n \"key\": \"colorFunction\",\r\n \"type\": \"javascript\"\r\n }, \"strokeWeight\", \"strokeOpacity\", \"showPoints\",{\r\n \"key\": \"pointColor\",\r\n \"type\": \"color\"\r\n }, \"pointSize\", {\r\n \"key\": \"defaultMarkerColor\",\r\n \"type\": \"color\"\r\n }, {\r\n \"key\": \"markerImage\",\r\n \"type\": \"image\"\r\n }, \"markerImageSize\", \"rotationAngle\",\"useMarkerImageFunction\",\r\n {\r\n \"key\":\"markerImageFunction\",\r\n \"type\":\"javascript\"\r\n }, {\r\n \"key\":\"markerImages\",\r\n \"items\":[\r\n {\r\n \"key\":\"markerImages[]\",\r\n \"type\":\"image\"\r\n }\r\n ]\r\n }]\r\n}",
+ "dataKeySettingsSchema": "{}",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"mapProvider\":\"OpenStreetMap.Mapnik\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"tooltipColor\":\"#fff\",\"tooltipFontColor\":\"#000\",\"tooltipOpacity\":1,\"tooltipPattern\":\"${entityName} Latitude: ${latitude:7}Longitude: ${longitude:7}End Time: ${maxTime}Start Time: ${minTime}\",\"strokeWeight\":3,\"strokeOpacity\":1,\"pointSize\":10,\"markerImageSize\":34,\"rotationAngle\":180},\"title\":\"Trip Animation\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
+ }
}
]
}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 0f40dd37f5..aa1f8bc909 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -118,17 +118,23 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
this.rpcSubscriptions = new HashMap<>();
this.toDeviceRpcPendingMap = new HashMap<>();
this.toServerRpcPendingMap = new HashMap<>();
- initAttributes();
- restoreSessions();
+ if (initAttributes()) {
+ restoreSessions();
+ }
}
- private void initAttributes() {
+ private boolean initAttributes() {
Device device = systemContext.getDeviceService().findDeviceById(tenantId, deviceId);
- this.deviceName = device.getName();
- this.deviceType = device.getType();
- this.defaultMetaData = new TbMsgMetaData();
- this.defaultMetaData.putValue("deviceName", deviceName);
- this.defaultMetaData.putValue("deviceType", deviceType);
+ if (device != null) {
+ this.deviceName = device.getName();
+ this.deviceType = device.getType();
+ this.defaultMetaData = new TbMsgMetaData();
+ this.defaultMetaData.putValue("deviceName", deviceName);
+ this.defaultMetaData.putValue("deviceType", deviceType);
+ return true;
+ } else {
+ return false;
+ }
}
void processRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
index 368634eb06..44b6f3b6c5 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
@@ -91,17 +91,19 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
- log.trace("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
- // Creating and starting the actors;
- for (RuleNode ruleNode : ruleNodeList) {
- log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
- ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
- nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+ if (ruleChain != null) {
+ ruleChainName = ruleChain.getName();
+ List ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
+ log.trace("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
+ // Creating and starting the actors;
+ for (RuleNode ruleNode : ruleNodeList) {
+ log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
+ ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+ nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+ }
+ initRoutes(ruleChain, ruleNodeList);
+ started = true;
}
- initRoutes(ruleChain, ruleNodeList);
- started = true;
} else {
onUpdate(context);
}
@@ -110,31 +112,33 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
- log.trace("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
- for (RuleNode ruleNode : ruleNodeList) {
- RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
- if (existing == null) {
- log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
- ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
- nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
- } else {
- log.trace("[{}][{}] Updating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
- existing.setSelf(ruleNode);
- existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self);
+ if (ruleChain != null) {
+ ruleChainName = ruleChain.getName();
+ List ruleNodeList = service.getRuleChainNodes(tenantId, entityId);
+ log.trace("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size());
+ for (RuleNode ruleNode : ruleNodeList) {
+ RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
+ if (existing == null) {
+ log.trace("[{}][{}] Creating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
+ ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+ nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+ } else {
+ log.trace("[{}][{}] Updating rule node [{}]: {}", entityId, ruleNode.getId(), ruleNode.getName(), ruleNode);
+ existing.setSelf(ruleNode);
+ existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self);
+ }
}
- }
- Set existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
- List removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList());
- removedRules.forEach(ruleNodeId -> {
- log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId);
- RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
- removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self);
- });
+ Set existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
+ List removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList());
+ removedRules.forEach(ruleNodeId -> {
+ log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId);
+ RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
+ removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self);
+ });
- initRoutes(ruleChain, ruleNodeList);
+ initRoutes(ruleChain, ruleNodeList);
+ }
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
index fc8ff3dc0a..a6543795ac 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -55,7 +55,9 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor componentClazz = Class.forName(ruleNode.getType());
- TbNode tbNode = (TbNode) (componentClazz.newInstance());
- tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
+ TbNode tbNode = null;
+ if (ruleNode != null) {
+ Class> componentClazz = Class.forName(ruleNode.getType());
+ tbNode = (TbNode) (componentClazz.newInstance());
+ tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
+ }
return tbNode;
}
diff --git a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
index c932f2133e..34c4e3c46d 100644
--- a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
+++ b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
@@ -22,7 +22,7 @@ import java.util.HashMap;
import java.util.Map;
@Configuration
-@ConfigurationProperties(prefix = "audit_log.logging_level")
+@ConfigurationProperties(prefix = "audit-log.logging-level")
public class AuditLogLevelProperties {
private Map mask = new HashMap<>();
diff --git a/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java
new file mode 100644
index 0000000000..b70f781acd
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.SchedulingConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+@Configuration
+@EnableScheduling
+public class SchedulingConfiguration implements SchedulingConfigurer {
+
+ @Override
+ public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
+ taskRegistrar.setScheduler(taskScheduler());
+ }
+
+ @Bean(destroyMethod="shutdown")
+ public TaskScheduler taskScheduler() {
+ ThreadPoolTaskScheduler threadPoolScheduler = new ThreadPoolTaskScheduler();
+ threadPoolScheduler.setThreadNamePrefix("TB-Scheduling-");
+ threadPoolScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
+ threadPoolScheduler.setRemoveOnCancelPolicy(true);
+ return threadPoolScheduler;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
index 2402a2f6a9..fc25af3a56 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
@@ -15,11 +15,28 @@
*/
package org.thingsboard.server.config;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.ExtendedProperties;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.ResourceBundleMessageSource;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.util.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
@Configuration
public class ThingsboardMessageConfiguration {
@@ -32,5 +49,114 @@ public class ThingsboardMessageConfiguration {
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
-
+
+ private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:/templates/";
+
+ private ResourceLoader resourceLoader = new DefaultResourceLoader();
+
+ @Bean
+ public VelocityEngine velocityEngine() {
+ VelocityEngine velocityEngine = new VelocityEngine();
+ try {
+ Resource resource = resourceLoader.getResource(DEFAULT_RESOURCE_LOADER_PATH);
+ File file = resource.getFile();
+ velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file");
+ velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true");
+ velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, file.getAbsolutePath());
+ } catch (IOException e) {
+ initSpringResourceLoader(velocityEngine, DEFAULT_RESOURCE_LOADER_PATH);
+ }
+ velocityEngine.init();
+ return velocityEngine;
+ }
+
+ private void initSpringResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
+ velocityEngine.setProperty(
+ RuntimeConstants.RESOURCE_LOADER, SpringResourceLoader.NAME);
+ velocityEngine.setProperty(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER_CLASS, SpringResourceLoader.class.getName());
+ velocityEngine.setProperty(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true");
+ velocityEngine.setApplicationAttribute(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER, resourceLoader);
+ velocityEngine.setApplicationAttribute(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER_PATH, resourceLoaderPath);
+ }
+
+ @Slf4j
+ static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader {
+
+ public static final String NAME = "spring";
+
+ public static final String SPRING_RESOURCE_LOADER_CLASS = "spring.resource.loader.class";
+
+ public static final String SPRING_RESOURCE_LOADER_CACHE = "spring.resource.loader.cache";
+
+ public static final String SPRING_RESOURCE_LOADER = "spring.resource.loader";
+
+ public static final String SPRING_RESOURCE_LOADER_PATH = "spring.resource.loader.path";
+
+ private org.springframework.core.io.ResourceLoader resourceLoader;
+
+ private String[] resourceLoaderPaths;
+
+
+ @Override
+ public void init(ExtendedProperties configuration) {
+ this.resourceLoader = (org.springframework.core.io.ResourceLoader)
+ this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER);
+ String resourceLoaderPath = (String) this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER_PATH);
+ if (this.resourceLoader == null) {
+ throw new IllegalArgumentException(
+ "'resourceLoader' application attribute must be present for SpringResourceLoader");
+ }
+ if (resourceLoaderPath == null) {
+ throw new IllegalArgumentException(
+ "'resourceLoaderPath' application attribute must be present for SpringResourceLoader");
+ }
+ this.resourceLoaderPaths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
+ for (int i = 0; i < this.resourceLoaderPaths.length; i++) {
+ String path = this.resourceLoaderPaths[i];
+ if (!path.endsWith("/")) {
+ this.resourceLoaderPaths[i] = path + "/";
+ }
+ }
+ if (log.isInfoEnabled()) {
+ log.info("SpringResourceLoader for Velocity: using resource loader [" + this.resourceLoader +
+ "] and resource loader paths " + Arrays.asList(this.resourceLoaderPaths));
+ }
+ }
+
+ @Override
+ public InputStream getResourceStream(String source) throws ResourceNotFoundException {
+ if (log.isDebugEnabled()) {
+ log.debug("Looking for Velocity resource with name [" + source + "]");
+ }
+ for (String resourceLoaderPath : this.resourceLoaderPaths) {
+ org.springframework.core.io.Resource resource =
+ this.resourceLoader.getResource(resourceLoaderPath + source);
+ try {
+ return resource.getInputStream();
+ }
+ catch (IOException ex) {
+ if (log.isDebugEnabled()) {
+ log.debug("Could not find Velocity resource: " + resource);
+ }
+ }
+ }
+ throw new ResourceNotFoundException(
+ "Could not find resource [" + source + "] in Spring resource loader path");
+ }
+
+ @Override
+ public boolean isSourceModified(org.apache.velocity.runtime.resource.Resource resource) {
+ return false;
+ }
+
+ @Override
+ public long getLastModified(org.apache.velocity.runtime.resource.Resource resource) {
+ return 0;
+ }
+
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 541e4bd17d..653ad68232 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -57,7 +57,7 @@ import java.util.List;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
-@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
+@Order(SecurityProperties.BASIC_AUTH_ORDER)
public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
index 711d4ce029..b8b703e4fc 100644
--- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
@@ -58,7 +58,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
- Map attributes) throws Exception {
+ Map attributes) throws Exception {
SecurityUser user = null;
try {
user = getCurrentUser();
@@ -73,7 +73,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
- Exception exception) {
+ Exception exception) {
//Do nothing
}
});
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index f8f02a9d3b..169bba8895 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -392,4 +392,4 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
-}
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java
index f6f43768a0..a0cedcf14f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.thingsboard.server.dao.cassandra.CassandraInstallCluster;
import org.thingsboard.server.service.install.cql.CQLStatementsParser;
@@ -30,6 +31,7 @@ public abstract class CassandraAbstractDatabaseSchemaService implements Database
private static final String CASSANDRA_DIR = "cassandra";
@Autowired
+ @Qualifier("CassandraInstallCluster")
private CassandraInstallCluster cluster;
@Autowired
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
index 13c198e6e5..1bab31d9e0 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.install;
import com.datastax.driver.core.KeyspaceMetadata;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
@@ -65,6 +66,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
private CassandraCluster cluster;
@Autowired
+ @Qualifier("CassandraInstallCluster")
private CassandraInstallCluster installCluster;
@Autowired
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index 7ade02bf19..217d62b54e 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -18,7 +18,9 @@ package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
@@ -26,7 +28,6 @@ import org.springframework.core.NestedRuntimeException;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
-import org.springframework.ui.velocity.VelocityEngineUtils;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@@ -39,6 +40,8 @@ import org.thingsboard.server.dao.settings.AdminSettingsService;
import javax.annotation.PostConstruct;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
+import java.io.StringWriter;
+import java.io.Writer;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -126,7 +129,7 @@ public class DefaultMailService implements MailService {
Map model = new HashMap();
model.put(TARGET_EMAIL, email);
- String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ String message = mergeTemplateIntoString(this.engine,
"test.vm", UTF_8, model);
sendMail(testMailSender, mailFrom, email, subject, message);
@@ -141,7 +144,7 @@ public class DefaultMailService implements MailService {
model.put("activationLink", activationLink);
model.put(TARGET_EMAIL, email);
- String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ String message = mergeTemplateIntoString(this.engine,
"activation.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message);
@@ -156,7 +159,7 @@ public class DefaultMailService implements MailService {
model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email);
- String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ String message = mergeTemplateIntoString(this.engine,
"account.activated.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message);
@@ -171,7 +174,7 @@ public class DefaultMailService implements MailService {
model.put("passwordResetLink", passwordResetLink);
model.put(TARGET_EMAIL, email);
- String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ String message = mergeTemplateIntoString(this.engine,
"reset.password.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message);
@@ -186,7 +189,7 @@ public class DefaultMailService implements MailService {
model.put("loginLink", loginLink);
model.put(TARGET_EMAIL, email);
- String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ String message = mergeTemplateIntoString(this.engine,
"password.was.reset.vm", UTF_8, model);
sendMail(mailSender, mailFrom, email, subject, message);
@@ -225,6 +228,22 @@ public class DefaultMailService implements MailService {
}
}
+ private static String mergeTemplateIntoString(VelocityEngine velocityEngine, String templateLocation,
+ String encoding, Map model) throws VelocityException {
+
+ StringWriter result = new StringWriter();
+ mergeTemplate(velocityEngine, templateLocation, encoding, model, result);
+ return result.toString();
+ }
+
+ private static void mergeTemplate(
+ VelocityEngine velocityEngine, String templateLocation, String encoding,
+ Map model, Writer writer) throws VelocityException {
+
+ VelocityContext velocityContext = new VelocityContext(model);
+ velocityEngine.mergeTemplate(templateLocation, encoding, velocityContext, writer);
+ }
+
protected ThingsboardException handleException(Exception exception) {
String message;
if (exception instanceof NestedRuntimeException) {
diff --git a/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java
index c50fb62194..e4062b1b78 100644
--- a/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java
+++ b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java
@@ -47,11 +47,4 @@ public class DefaultDeviceSessionCacheService implements DeviceSessionCacheServi
log.debug("[{}] Pushing session data to cache: {}", deviceId, sessions);
return sessions;
}
-
- public static void main (String[] args){
- UUID uuid = UUID.fromString("d5db434e-9cd2-4903-8b3b-421b2d93664d");
- System.out.println(uuid.getMostSignificantBits());
- System.out.println(uuid.getLeastSignificantBits());
- }
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
index e6b28b33cd..86747150c9 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
@@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
-import org.springframework.web.socket.WebSocketSession;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 235646f6c2..72b8159407 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -293,11 +293,13 @@ spring.mvc.cors:
# spring serve gzip compressed static resources
spring.resources.chain:
- gzipped: "true"
+ compressed: "true"
strategy:
content:
enabled: "true"
+spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true"
+
# HSQLDB DAO Configuration
spring:
data:
@@ -331,7 +333,7 @@ spring:
# password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
# Audit log parameters
-audit_log:
+audit-log:
# Enable/disable audit log functionality.
enabled: "${AUDIT_LOG_ENABLED:true}"
# Specify partitioning size for audit log by tenant id storage. Example MINUTES, HOURS, DAYS, MONTHS
@@ -340,7 +342,7 @@ audit_log:
default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}"
# Logging levels per each entity type.
# Allowed values: OFF (disable), W (log write operations), RW (log read and write operations)
- logging_level:
+ logging-level:
mask:
"device": "${AUDIT_LOG_MASK_DEVICE:W}"
"asset": "${AUDIT_LOG_MASK_ASSET:W}"
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
index 218d2e64aa..67efdcdb8b 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
@@ -128,7 +128,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
- String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(),
@@ -183,7 +183,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
- String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(),
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java
index 8e8639c50c..50de5727f9 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java
@@ -111,7 +111,7 @@ public abstract class AbstractMqttTelemetryIntegrationTest extends AbstractContr
client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value());
String payload = "{\"key\":\"value\"}";
String result = doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk());
- latch.await(3, TimeUnit.SECONDS);
+ latch.await(10, TimeUnit.SECONDS);
assertEquals(payload, callback.getPayload());
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
index 519607b840..d1395f43db 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
@@ -18,11 +18,7 @@ package org.thingsboard.server.dao;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.dao.model.ToData;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
public abstract class DaoUtil {
@@ -50,6 +46,14 @@ public abstract class DaoUtil {
return object;
}
+ public static T getData(Optional extends ToData> data) {
+ T object = null;
+ if (data.isPresent()) {
+ object = data.get().toData();
+ }
+ return object;
+ }
+
public static UUID getId(UUIDBased idBased) {
UUID id = null;
if (idBased != null) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index c5518bef9d..ad3db8781f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -60,7 +60,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Slf4j
@Service
-@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true")
+@ConditionalOnProperty(prefix = "audit-log", value = "enabled", havingValue = "true")
public class AuditLogServiceImpl implements AuditLogService {
private static final ObjectMapper objectMapper = new ObjectMapper();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
index 860e0a962e..6ffaf1ea00 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -88,11 +88,11 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao redisTemplate(RedisConnectionFactory cf) {
- RedisTemplate redisTemplate = new RedisTemplate<>();
- redisTemplate.setConnectionFactory(cf);
- return redisTemplate;
- }
-
- @Bean
- public CacheManager cacheManager(RedisTemplate redisTemplate) {
- return new RedisCacheManager(redisTemplate);
+ public CacheManager cacheManager(RedisConnectionFactory cf) {
+ DefaultFormattingConversionService redisConversionService = new DefaultFormattingConversionService();
+ RedisCacheConfiguration.registerDefaultConverters(redisConversionService);
+ registerDefaultConverters(redisConversionService);
+ RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().withConversionService(redisConversionService);
+ return RedisCacheManager.builder(cf).cacheDefaults(configuration).build();
}
@Bean
@@ -73,5 +93,8 @@ public class TBRedisCacheConfiguration {
return new PreviousDeviceCredentialsIdKeyGenerator();
}
-
+ private static void registerDefaultConverters(ConverterRegistry registry) {
+ Assert.notNull(registry, "ConverterRegistry must not be null!");
+ registry.addConverter(EntityId.class, String.class, EntityId::toString);
+ }
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java
index 84073f62cd..4fa383656b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java
@@ -21,7 +21,7 @@ import org.thingsboard.server.dao.util.NoSqlAnyDao;
import javax.annotation.PostConstruct;
-@Component
+@Component("CassandraCluster")
@NoSqlAnyDao
public class CassandraCluster extends AbstractCassandraCluster {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java
index 54365e5281..2492552c99 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java
@@ -21,7 +21,7 @@ import org.thingsboard.server.dao.util.NoSqlAnyDao;
import javax.annotation.PostConstruct;
-@Component
+@Component("CassandraInstallCluster")
@NoSqlAnyDao
@Profile("install")
public class CassandraInstallCluster extends AbstractCassandraCluster {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
index 813017ff8e..de0ec87359 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
@@ -140,7 +140,7 @@ public class AuditLogEntity extends BaseSqlEntity implements BaseEntit
auditLog.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString()));
}
if (userId != null) {
- auditLog.setUserId(new UserId(toUUID(entityId)));
+ auditLog.setUserId(new UserId(toUUID(userId)));
}
auditLog.setEntityName(this.entityName);
auditLog.setUserName(this.userName);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
index b0840140e3..09d4e53928 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
@@ -27,6 +27,7 @@ import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.exceptions.CodecNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.model.type.AuthorityCodec;
@@ -44,6 +45,7 @@ import java.util.concurrent.ConcurrentMap;
public abstract class CassandraAbstractDao {
@Autowired
+ @Qualifier("CassandraCluster")
protected CassandraCluster cluster;
private ConcurrentMap preparedStatementMap = new ConcurrentHashMap<>();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
index 3b0bec9064..34c0a7cb25 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.BaseEntity;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@@ -67,23 +68,23 @@ public abstract class JpaAbstractDao, D>
@Override
public D findById(TenantId tenantId, UUID key) {
log.debug("Get entity by key {}", key);
- E entity = getCrudRepository().findOne(fromTimeUUID(key));
+ Optional entity = getCrudRepository().findById(fromTimeUUID(key));
return DaoUtil.getData(entity);
}
@Override
public ListenableFuture findByIdAsync(TenantId tenantId, UUID key) {
log.debug("Get entity by key async {}", key);
- return service.submit(() -> DaoUtil.getData(getCrudRepository().findOne(fromTimeUUID(key))));
+ return service.submit(() -> DaoUtil.getData(getCrudRepository().findById(fromTimeUUID(key))));
}
@Override
@Transactional
public boolean removeById(TenantId tenantId, UUID id) {
String key = fromTimeUUID(id);
- getCrudRepository().delete(key);
+ getCrudRepository().deleteById(key);
log.debug("Remove request: {}", key);
- return getCrudRepository().findOne(key) == null;
+ return !getCrudRepository().existsById(key);
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
index c2cfee19d3..dc65018cf1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
@@ -52,7 +52,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
AttributeKvCompositeKey compositeKey =
getAttributeKvCompositeKey(entityId, attributeType, attributeKey);
return Futures.immediateFuture(
- Optional.ofNullable(DaoUtil.getData(attributeKvRepository.findOne(compositeKey))));
+ Optional.ofNullable(DaoUtil.getData(attributeKvRepository.findById(compositeKey))));
}
@Override
@@ -64,7 +64,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
getAttributeKvCompositeKey(entityId, attributeType, attributeKey))
.collect(Collectors.toList());
return Futures.immediateFuture(
- DaoUtil.convertDataList(Lists.newArrayList(attributeKvRepository.findAll(compositeKeys))));
+ DaoUtil.convertDataList(Lists.newArrayList(attributeKvRepository.findAllById(compositeKeys))));
}
@Override
@@ -103,7 +103,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
}).collect(Collectors.toList());
return service.submit(() -> {
- attributeKvRepository.delete(entitiesToDelete);
+ attributeKvRepository.deleteAll(entitiesToDelete);
return null;
});
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java
index 1cc7104365..c66ea9c5cc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java
@@ -66,7 +66,7 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
- return service.submit(() -> relationRepository.findOne(key) != null);
+ return service.submit(() -> relationRepository.existsById(key));
}
@Override
public ListenableFuture getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
- return service.submit(() -> DaoUtil.getData(relationRepository.findOne(key)));
+ return service.submit(() -> DaoUtil.getData(relationRepository.findById(key)));
}
private RelationCompositeKey getRelationCompositeKey(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
@@ -152,9 +152,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
}
private boolean deleteRelationIfExists(RelationCompositeKey key) {
- boolean relationExistsBeforeDelete = relationRepository.exists(key);
+ boolean relationExistsBeforeDelete = relationRepository.existsById(key);
if (relationExistsBeforeDelete) {
- relationRepository.delete(key);
+ relationRepository.deleteById(key);
}
return relationExistsBeforeDelete;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java
index 208b63a0b0..e2fb1ff213 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java
@@ -53,7 +53,7 @@ public interface RelationRepository
RelationEntity save(RelationEntity entity);
@Transactional
- void delete(RelationCompositeKey id);
+ void deleteById(RelationCompositeKey id);
@Transactional
void deleteByFromIdAndFromType(String fromId, String fromType);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 92e5c1db0d..2b36eb80b9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
@@ -284,10 +284,10 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
entityId.getEntityType(),
fromTimeUUID(entityId.getId()),
key);
- TsKvLatestEntity entry = tsKvLatestRepository.findOne(compositeKey);
+ Optional entry = tsKvLatestRepository.findById(compositeKey);
TsKvEntry result;
- if (entry != null) {
- result = DaoUtil.getData(entry);
+ if (entry.isPresent()) {
+ result = DaoUtil.getData(entry.get());
} else {
result = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
index b57cd33c6c..c74dbd54e5 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
@@ -82,6 +82,7 @@ public class CustomCassandraCQLUnit extends BaseCassandraUnit {
session = null;
cluster = null;
}
+ System.setSecurityManager(null);
}
// Getters for those who do not like to directly access fields
diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties
index 7440a966fe..d8f18e090f 100644
--- a/dao/src/test/resources/application-test.properties
+++ b/dao/src/test/resources/application-test.properties
@@ -4,10 +4,10 @@ zk.zk_dir=/thingsboard
updates.enabled=false
-audit_log.enabled=true
-audit_log.by_tenant_partitioning=MONTHS
-audit_log.default_query_period=30
-audit_log.sink.type=none
+audit-log.enabled=true
+audit-log.by_tenant_partitioning=MONTHS
+audit-log.default_query_period=30
+audit-log.sink.type=none
cache.type=caffeine
#cache.type=redis
diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties
index 3357425fce..745aa9e1e0 100644
--- a/dao/src/test/resources/sql-test.properties
+++ b/dao/src/test/resources/sql-test.properties
@@ -4,6 +4,7 @@ database.entities.type=sql
sql.ts_inserts_executor_type=fixed
sql.ts_inserts_fixed_thread_pool_size=10
+spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect
diff --git a/k8s/.env b/k8s/.env
new file mode 100644
index 0000000000..e50e57d9b8
--- /dev/null
+++ b/k8s/.env
@@ -0,0 +1,5 @@
+
+# Database used by ThingsBoard, can be either postgres (PostgreSQL) or cassandra (Cassandra).
+# According to the database type corresponding kubernetes resources will be deployed (see postgres.yml, cassandra.yml for details).
+
+DATABASE=postgres
diff --git a/k8s/README.md b/k8s/README.md
new file mode 100644
index 0000000000..e57790ffc4
--- /dev/null
+++ b/k8s/README.md
@@ -0,0 +1,100 @@
+# Kubernetes resources configuration for ThingsBoard Microservices
+
+This folder containing scripts and Kubernetes resources configurations to run ThingsBoard in Microservices mode.
+
+## Prerequisites
+
+ThingsBoard Microservices are running on Kubernetes cluster.
+You need to have a Kubernetes cluster, and the kubectl command-line tool must be configured to communicate with your cluster.
+If you do not already have a cluster, you can create one by using [Minikube](https://kubernetes.io/docs/setup/minikube),
+or you can choose any other available [Kubernetes cluster deployment solutions](https://kubernetes.io/docs/setup/pick-right-solution/).
+
+## Installation
+
+Before performing initial installation you can configure the type of database to be used with ThingsBoard.
+In order to set database type change the value of `DATABASE` variable in `.env` file to one of the following:
+
+- `postgres` - use PostgreSQL database;
+- `cassandra` - use Cassandra database;
+
+**NOTE**: According to the database type corresponding kubernetes resources will be deployed (see `postgres.yml`, `cassandra.yml` for details).
+
+Execute the following command to run installation:
+
+`
+$ ./k8s-install-tb.sh --loadDemo
+`
+
+Where:
+
+- `--loadDemo` - optional argument. Whether to load additional demo data.
+
+## Running
+
+Execute the following command to deploy resources:
+
+`
+$ ./k8s-deploy-resources.sh
+`
+
+After a while when all resources will be successfully started you can open `http://{your-cluster-ip}` in you browser (for ex. `http://192.168.99.101`).
+You should see ThingsBoard login page.
+
+Use the following default credentials:
+
+- **System Administrator**: sysadmin@thingsboard.org / sysadmin
+
+If you installed DataBase with demo data (using `--loadDemo` flag) you can also use the following credentials:
+
+- **Tenant Administrator**: tenant@thingsboard.org / tenant
+- **Customer User**: customer@thingsboard.org / customer
+
+In case of any issues you can examine service logs for errors.
+For example to see ThingsBoard node logs execute the following commands:
+
+1) Get list of the running tb-node pods:
+
+`
+$ kubectl get pods -l app=tb-node
+`
+
+2) Fetch logs of tb-node pod:
+
+`
+$ kubectl logs -f [tb-node-pod-name]
+`
+
+Where:
+
+- `tb-node-pod-name` - tb-node pod name obtained from the list of the running tb-node pods.
+
+Or use `kubectl get pods` to see the state of all the pods.
+Or use `kubectl get services` to see the state of all the services.
+Or use `kubectl get deployments` to see the state of all the deployments.
+See [kubectl Cheat Sheet](https://kubernetes.io/docs/reference/kubectl/cheatsheet/) command reference for details.
+
+Execute the following command to delete all deployed microservices:
+
+`
+$ ./k8s-delete-resources.sh
+`
+
+Execute the following command to delete all resources (including database):
+
+`
+$ ./k8s-delete-all.sh
+`
+
+## Upgrading
+
+In case when database upgrade is needed, execute the following commands:
+
+```
+$ ./k8s-delete-resources.sh
+$ ./k8s-upgrade-tb.sh --fromVersion=[FROM_VERSION]
+$ ./k8s-deploy-resources.sh
+```
+
+Where:
+
+- `FROM_VERSION` - from which version upgrade should be started. See [Upgrade Instructions](https://thingsboard.io/docs/user-guide/install/upgrade-instructions) for valid `fromVersion` values.
diff --git a/k8s/cassandra.yml b/k8s/cassandra.yml
new file mode 100644
index 0000000000..aa1d18022c
--- /dev/null
+++ b/k8s/cassandra.yml
@@ -0,0 +1,164 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: storage.k8s.io/v1
+kind: StorageClass
+metadata:
+ name: fast
+ namespace: thingsboard
+provisioner: k8s.io/minikube-hostpath
+parameters:
+ type: pd-ssd
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: cassandra-probe-config
+ namespace: thingsboard
+ labels:
+ name: cassandra-probe-config
+data:
+ probe: |
+ if [[ $(nodetool status | grep $POD_IP) == *"UN"* ]]; then
+ if [[ $DEBUG ]]; then
+ echo "UN";
+ fi
+ exit 0;
+ else
+ if [[ $DEBUG ]]; then
+ echo "Not Up";
+ fi
+ exit 1;
+ fi
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: cassandra
+ namespace: thingsboard
+ labels:
+ app: cassandra
+spec:
+ serviceName: cassandra
+ replicas: 1
+ selector:
+ matchLabels:
+ app: cassandra
+ template:
+ metadata:
+ labels:
+ app: cassandra
+ spec:
+ volumes:
+ - name: cassandra-probe-config
+ configMap:
+ name: cassandra-probe-config
+ items:
+ - key: probe
+ path: ready-probe.sh
+ mode: 0777
+ terminationGracePeriodSeconds: 1800
+ containers:
+ - name: cassandra
+ image: cassandra:3.11.3
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 7000
+ name: intra-node
+ - containerPort: 7001
+ name: tls-intra-node
+ - containerPort: 7199
+ name: jmx
+ - containerPort: 9042
+ name: cql
+ - containerPort: 9160
+ name: thrift
+ resources:
+ limits:
+ cpu: "1000m"
+ memory: 2Gi
+ requests:
+ cpu: "1000m"
+ memory: 2Gi
+ securityContext:
+ capabilities:
+ add:
+ - IPC_LOCK
+ lifecycle:
+ preStop:
+ exec:
+ command:
+ - /bin/sh
+ - -c
+ - nodetool drain
+ env:
+ - name: CASSANDRA_SEEDS
+ value: "cassandra-0.cassandra.thingsboard.svc.cluster.local"
+ - name: MAX_HEAP_SIZE
+ value: 1024M
+ - name: HEAP_NEWSIZE
+ value: 256M
+ - name: CASSANDRA_CLUSTER_NAME
+ value: "Thingsboard Cluster"
+ - name: CASSANDRA_DC
+ value: "DC1-Thingsboard-Cluster"
+ - name: CASSANDRA_RACK
+ value: "Rack-Thingsboard-Cluster"
+ - name: CASSANDRA_AUTO_BOOTSTRAP
+ value: "false"
+ - name: CASSANDRA_ENDPOINT_SNITCH
+ value: GossipingPropertyFileSnitch
+ - name: POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ readinessProbe:
+ exec:
+ command:
+ - /bin/bash
+ - -c
+ - /probe/ready-probe.sh
+ initialDelaySeconds: 60
+ timeoutSeconds: 5
+ volumeMounts:
+ - name: cassandra-probe-config
+ mountPath: /probe
+ - name: cassandra-data
+ mountPath: /var/lib/cassandra
+ volumeClaimTemplates:
+ - metadata:
+ name: cassandra-data
+ spec:
+ accessModes: [ "ReadWriteOnce" ]
+ storageClassName: fast
+ resources:
+ requests:
+ storage: 1Gi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: cassandra
+ name: cassandra
+ namespace: thingsboard
+spec:
+ clusterIP: None
+ ports:
+ - port: 9042
+ selector:
+ app: cassandra
+---
diff --git a/k8s/database-setup.yml b/k8s/database-setup.yml
new file mode 100644
index 0000000000..d73a685b74
--- /dev/null
+++ b/k8s/database-setup.yml
@@ -0,0 +1,43 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: Pod
+metadata:
+ name: tb-db-setup
+ namespace: thingsboard
+spec:
+ volumes:
+ - name: tb-node-config
+ configMap:
+ name: tb-node-config
+ items:
+ - key: conf
+ path: thingsboard.conf
+ - key: logback
+ path: logback.xml
+ containers:
+ - name: tb-db-setup
+ imagePullPolicy: Always
+ image: thingsboard/tb-node:latest
+ envFrom:
+ - configMapRef:
+ name: tb-node-db-config
+ volumeMounts:
+ - mountPath: /config
+ name: tb-node-config
+ command: ['sh', '-c', 'while [ ! -f /install-finished ]; do sleep 2; done;']
+ restartPolicy: Never
diff --git a/k8s/k8s-delete-all.sh b/k8s/k8s-delete-all.sh
new file mode 100755
index 0000000000..a90829368e
--- /dev/null
+++ b/k8s/k8s-delete-all.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+kubectl -n thingsboard delete svc,sts,deploy,pv,pvc,cm,po,ing --all --include-uninitialized
diff --git a/k8s/k8s-delete-resources.sh b/k8s/k8s-delete-resources.sh
new file mode 100755
index 0000000000..87f25ebedd
--- /dev/null
+++ b/k8s/k8s-delete-resources.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -e
+
+kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
+kubectl delete -f thingsboard.yml
diff --git a/k8s/k8s-deploy-resources.sh b/k8s/k8s-deploy-resources.sh
new file mode 100755
index 0000000000..86ec235dd7
--- /dev/null
+++ b/k8s/k8s-deploy-resources.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -e
+
+kubectl apply -f tb-namespace.yml
+kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
+kubectl apply -f tb-node-configmap.yml
+kubectl apply -f tb-mqtt-transport-configmap.yml
+kubectl apply -f tb-http-transport-configmap.yml
+kubectl apply -f tb-coap-transport-configmap.yml
+kubectl apply -f thingsboard.yml
diff --git a/k8s/k8s-install-tb.sh b/k8s/k8s-install-tb.sh
new file mode 100755
index 0000000000..9fa9c0ea3a
--- /dev/null
+++ b/k8s/k8s-install-tb.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+function installTb() {
+
+ loadDemo=$1
+
+ kubectl apply -f tb-node-configmap.yml
+ kubectl apply -f database-setup.yml &&
+ kubectl wait --for=condition=Ready pod/tb-db-setup --timeout=120s &&
+ kubectl exec tb-db-setup -- sh -c 'export INSTALL_TB=true; export LOAD_DEMO='"$loadDemo"'; start-tb-node.sh; touch /install-finished;'
+
+ kubectl delete pod tb-db-setup
+
+}
+
+function installPostgres() {
+
+ kubectl apply -f postgres.yml
+ kubectl apply -f tb-node-postgres-configmap.yml
+
+ kubectl rollout status deployment/postgres
+}
+
+function installCassandra() {
+
+ kubectl apply -f cassandra.yml
+ kubectl apply -f tb-node-cassandra-configmap.yml
+
+ kubectl rollout status statefulset/cassandra
+
+ kubectl exec -it cassandra-0 -- bash -c "cqlsh -e \
+ \"CREATE KEYSPACE IF NOT EXISTS thingsboard \
+ WITH replication = { \
+ 'class' : 'SimpleStrategy', \
+ 'replication_factor' : 1 \
+ };\""
+}
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+
+case $key in
+ --loadDemo)
+ LOAD_DEMO=true
+ shift # past argument
+ ;;
+ *)
+ # unknown option
+ ;;
+esac
+shift # past argument or value
+done
+
+if [ "$LOAD_DEMO" == "true" ]; then
+ loadDemo=true
+else
+ loadDemo=false
+fi
+
+source .env
+
+kubectl apply -f tb-namespace.yml
+kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
+
+case $DATABASE in
+ postgres)
+ installPostgres
+ installTb ${loadDemo}
+ ;;
+ cassandra)
+ installCassandra
+ installTb ${loadDemo}
+ ;;
+ *)
+ echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or cassandra." >&2
+ exit 1
+esac
diff --git a/k8s/k8s-upgrade-tb.sh b/k8s/k8s-upgrade-tb.sh
new file mode 100755
index 0000000000..35dc11f5d1
--- /dev/null
+++ b/k8s/k8s-upgrade-tb.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+for i in "$@"
+do
+case $i in
+ --fromVersion=*)
+ FROM_VERSION="${i#*=}"
+ shift
+ ;;
+ *)
+ # unknown option
+ ;;
+esac
+done
+
+if [[ -z "${FROM_VERSION// }" ]]; then
+ echo "--fromVersion parameter is invalid or unspecified!"
+ echo "Usage: k8s-upgrade-tb.sh --fromVersion={VERSION}"
+ exit 1
+else
+ fromVersion="${FROM_VERSION// }"
+fi
+
+kubectl apply -f database-setup.yml &&
+kubectl wait --for=condition=Ready pod/tb-db-setup --timeout=120s &&
+kubectl exec tb-db-setup -- sh -c 'export UPGRADE_TB=true; export FROM_VERSION='"$fromVersion"'; start-tb-node.sh; touch /install-finished;'
+
+kubectl delete pod tb-db-setup
diff --git a/k8s/postgres.yml b/k8s/postgres.yml
new file mode 100644
index 0000000000..2e7f2cc815
--- /dev/null
+++ b/k8s/postgres.yml
@@ -0,0 +1,95 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: postgres-pv-claim
+ namespace: thingsboard
+ labels:
+ app: postgres
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: postgres
+ namespace: thingsboard
+ labels:
+ app: postgres
+spec:
+ template:
+ metadata:
+ labels:
+ app: postgres
+ spec:
+ volumes:
+ - name: postgres-data
+ persistentVolumeClaim:
+ claimName: postgres-pv-claim
+ containers:
+ - name: postgres
+ imagePullPolicy: Always
+ image: postgres:9.6
+ ports:
+ - containerPort: 5432
+ name: postgres
+ env:
+ - name: POSTGRES_DB
+ value: "thingsboard"
+ volumeMounts:
+ - mountPath: /var/lib/postgresql/data
+ name: postgres-data
+ livenessProbe:
+ exec:
+ command:
+ - pg_isready
+ - -h
+ - localhost
+ - -U
+ - postgres
+ initialDelaySeconds: 60
+ timeoutSeconds: 30
+ readinessProbe:
+ exec:
+ command:
+ - pg_isready
+ - -h
+ - localhost
+ - -U
+ - postgres
+ initialDelaySeconds: 5
+ timeoutSeconds: 1
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-database
+ namespace: thingsboard
+spec:
+ type: ClusterIP
+ selector:
+ app: postgres
+ ports:
+ - port: 5432
+ name: postgres
+---
diff --git a/k8s/tb-coap-transport-configmap.yml b/k8s/tb-coap-transport-configmap.yml
new file mode 100644
index 0000000000..5381f766f4
--- /dev/null
+++ b/k8s/tb-coap-transport-configmap.yml
@@ -0,0 +1,65 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: tb-coap-transport-config
+ namespace: thingsboard
+ labels:
+ name: tb-coap-transport-config
+data:
+ conf: |
+ export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-coap-transport/${TB_HOST}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_HOST}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
+ export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
+ export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
+ export LOG_FILENAME=tb-coap-transport.out
+ export LOADER_PATH=/usr/share/tb-coap-transport/conf
+ logback: |
+
+
+
+
+ /var/log/tb-coap-transport/${TB_HOST}/tb-coap-transport.log
+
+ /var/log/tb-coap-transport/${TB_HOST}/tb-coap-transport.%d{yyyy-MM-dd}.%i.log
+ 100MB
+ 30
+ 3GB
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k8s/tb-http-transport-configmap.yml b/k8s/tb-http-transport-configmap.yml
new file mode 100644
index 0000000000..5376b82cef
--- /dev/null
+++ b/k8s/tb-http-transport-configmap.yml
@@ -0,0 +1,65 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: tb-http-transport-config
+ namespace: thingsboard
+ labels:
+ name: tb-http-transport-config
+data:
+ conf: |
+ export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-http-transport/${TB_HOST}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_HOST}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
+ export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
+ export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
+ export LOG_FILENAME=tb-http-transport.out
+ export LOADER_PATH=/usr/share/tb-http-transport/conf
+ logback: |
+
+
+
+
+ /var/log/tb-http-transport/${TB_HOST}/tb-http-transport.log
+
+ /var/log/tb-http-transport/${TB_HOST}/tb-http-transport.%d{yyyy-MM-dd}.%i.log
+ 100MB
+ 30
+ 3GB
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k8s/tb-mqtt-transport-configmap.yml b/k8s/tb-mqtt-transport-configmap.yml
new file mode 100644
index 0000000000..1e73a08db7
--- /dev/null
+++ b/k8s/tb-mqtt-transport-configmap.yml
@@ -0,0 +1,65 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: tb-mqtt-transport-config
+ namespace: thingsboard
+ labels:
+ name: tb-mqtt-transport-config
+data:
+ conf: |
+ export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-mqtt-transport/${TB_HOST}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_HOST}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
+ export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
+ export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
+ export LOG_FILENAME=tb-mqtt-transport.out
+ export LOADER_PATH=/usr/share/tb-mqtt-transport/conf
+ logback: |
+
+
+
+
+ /var/log/tb-mqtt-transport/${TB_HOST}/tb-mqtt-transport.log
+
+ /var/log/tb-mqtt-transport/${TB_HOST}/tb-mqtt-transport.%d{yyyy-MM-dd}.%i.log
+ 100MB
+ 30
+ 3GB
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k8s/tb-namespace.yml b/k8s/tb-namespace.yml
new file mode 100644
index 0000000000..113009635f
--- /dev/null
+++ b/k8s/tb-namespace.yml
@@ -0,0 +1,22 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: thingsboard
+ labels:
+ name: thingsboard
diff --git a/k8s/tb-node-cassandra-configmap.yml b/k8s/tb-node-cassandra-configmap.yml
new file mode 100644
index 0000000000..21a05bd6f6
--- /dev/null
+++ b/k8s/tb-node-cassandra-configmap.yml
@@ -0,0 +1,28 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: tb-node-db-config
+ namespace: thingsboard
+ labels:
+ name: tb-node-db-config
+data:
+ DATABASE_TS_TYPE: cassandra
+ DATABASE_ENTITIES_TYPE: cassandra
+ CASSANDRA_URL: cassandra:9042
+ CASSANDRA_SOCKET_READ_TIMEOUT: "60000"
diff --git a/k8s/tb-node-configmap.yml b/k8s/tb-node-configmap.yml
new file mode 100644
index 0000000000..550a0665d1
--- /dev/null
+++ b/k8s/tb-node-configmap.yml
@@ -0,0 +1,67 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: tb-node-config
+ namespace: thingsboard
+ labels:
+ name: tb-node-config
+data:
+ conf: |
+ export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data"
+ export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/thingsboard/${TB_HOST}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_HOST}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
+ export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
+ export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
+ export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
+ export LOG_FILENAME=thingsboard.out
+ export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions
+ logback: |
+
+
+
+
+ /var/log/thingsboard/${TB_HOST}/thingsboard.log
+
+ /var/log/thingsboard/${TB_HOST}/thingsboard.%d{yyyy-MM-dd}.%i.log
+ 100MB
+ 30
+ 3GB
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/k8s/tb-node-postgres-configmap.yml b/k8s/tb-node-postgres-configmap.yml
new file mode 100644
index 0000000000..3e07cb863c
--- /dev/null
+++ b/k8s/tb-node-postgres-configmap.yml
@@ -0,0 +1,31 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: tb-node-db-config
+ namespace: thingsboard
+ labels:
+ name: tb-node-db-config
+data:
+ DATABASE_TS_TYPE: sql
+ DATABASE_ENTITIES_TYPE: sql
+ SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQLDialect
+ SPRING_DRIVER_CLASS_NAME: org.postgresql.Driver
+ SPRING_DATASOURCE_URL: jdbc:postgresql://tb-database:5432/thingsboard
+ SPRING_DATASOURCE_USERNAME: postgres
+ SPRING_DATASOURCE_PASSWORD: postgres
diff --git a/k8s/thingsboard.yml b/k8s/thingsboard.yml
new file mode 100644
index 0000000000..0022e72d77
--- /dev/null
+++ b/k8s/thingsboard.yml
@@ -0,0 +1,608 @@
+#
+# Copyright © 2016-2019 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: zookeeper
+ namespace: thingsboard
+spec:
+ template:
+ metadata:
+ labels:
+ app: zookeeper
+ spec:
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: zookeeper:3.5
+ ports:
+ - containerPort: 2181
+ readinessProbe:
+ periodSeconds: 5
+ tcpSocket:
+ port: 2181
+ livenessProbe:
+ periodSeconds: 5
+ tcpSocket:
+ port: 2181
+ env:
+ - name: ZOO_MY_ID
+ value: "1"
+ - name: ZOO_SERVERS
+ value: "server.1=0.0.0.0:2888:3888;0.0.0.0:2181"
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: zookeeper
+ namespace: thingsboard
+spec:
+ type: ClusterIP
+ selector:
+ app: zookeeper
+ ports:
+ - name: zk-port
+ port: 2181
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-kafka
+ namespace: thingsboard
+spec:
+ template:
+ metadata:
+ labels:
+ app: tb-kafka
+ spec:
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: wurstmeister/kafka
+ ports:
+ - containerPort: 9092
+ readinessProbe:
+ periodSeconds: 20
+ tcpSocket:
+ port: 9092
+ livenessProbe:
+ periodSeconds: 5
+ tcpSocket:
+ port: 9092
+ env:
+ - name: KAFKA_ZOOKEEPER_CONNECT
+ value: "zookeeper:2181"
+ - name: KAFKA_LISTENERS
+ value: "INSIDE://:9093,OUTSIDE://:9092"
+ - name: KAFKA_ADVERTISED_LISTENERS
+ value: "INSIDE://:9093,OUTSIDE://tb-kafka:9092"
+ - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP
+ value: "INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT"
+ - name: KAFKA_INTER_BROKER_LISTENER_NAME
+ value: "INSIDE"
+ - name: KAFKA_CREATE_TOPICS
+ value: "js.eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.rule-engine:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600"
+ - name: KAFKA_AUTO_CREATE_TOPICS_ENABLE
+ value: "false"
+ - name: KAFKA_LOG_RETENTION_BYTES
+ value: "1073741824"
+ - name: KAFKA_LOG_SEGMENT_BYTES
+ value: "268435456"
+ - name: KAFKA_LOG_RETENTION_MS
+ value: "300000"
+ - name: KAFKA_LOG_CLEANUP_POLICY
+ value: "delete"
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-kafka
+ namespace: thingsboard
+spec:
+ type: ClusterIP
+ selector:
+ app: tb-kafka
+ ports:
+ - name: tb-kafka-port
+ port: 9092
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-redis
+ namespace: thingsboard
+spec:
+ template:
+ metadata:
+ labels:
+ app: tb-redis
+ spec:
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: redis:4.0
+ ports:
+ - containerPort: 6379
+ readinessProbe:
+ periodSeconds: 5
+ tcpSocket:
+ port: 6379
+ livenessProbe:
+ periodSeconds: 5
+ tcpSocket:
+ port: 6379
+ volumeMounts:
+ - mountPath: /data
+ name: redis-data
+ volumes:
+ - name: redis-data
+ emptyDir: {}
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-redis
+ namespace: thingsboard
+spec:
+ type: ClusterIP
+ selector:
+ app: tb-redis
+ ports:
+ - name: tb-redis-port
+ port: 6379
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-js-executor
+ namespace: thingsboard
+spec:
+ replicas: 20
+ selector:
+ matchLabels:
+ app: tb-js-executor
+ template:
+ metadata:
+ labels:
+ app: tb-js-executor
+ spec:
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: thingsboard/tb-js-executor:latest
+ env:
+ - name: REMOTE_JS_EVAL_REQUEST_TOPIC
+ value: "js.eval.requests"
+ - name: TB_KAFKA_SERVERS
+ value: "tb-kafka:9092"
+ - name: LOGGER_LEVEL
+ value: "info"
+ - name: LOG_FOLDER
+ value: "logs"
+ - name: LOGGER_FILENAME
+ value: "tb-js-executor-%DATE%.log"
+ - name: DOCKER_MODE
+ value: "true"
+ - name: SCRIPT_BODY_TRACE_FREQUENCY
+ value: "1000"
+ restartPolicy: Always
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-node
+ namespace: thingsboard
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: tb-node
+ template:
+ metadata:
+ labels:
+ app: tb-node
+ spec:
+ volumes:
+ - name: tb-node-config
+ configMap:
+ name: tb-node-config
+ items:
+ - key: conf
+ path: thingsboard.conf
+ - key: logback
+ path: logback.xml
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: thingsboard/tb-node:latest
+ ports:
+ - containerPort: 8080
+ name: http
+ - containerPort: 9001
+ name: rpc
+ env:
+ - name: RPC_HOST
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CLUSTER_NODE_ID
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: TB_HOST
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: ZOOKEEPER_ENABLED
+ value: "true"
+ - name: ZOOKEEPER_URL
+ value: "zookeeper:2181"
+ - name: TB_KAFKA_SERVERS
+ value: "tb-kafka:9092"
+ - name: JS_EVALUATOR
+ value: "remote"
+ - name: TRANSPORT_TYPE
+ value: "remote"
+ - name: CACHE_TYPE
+ value: "redis"
+ - name: REDIS_HOST
+ value: "tb-redis"
+ - name: HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE
+ value: "false"
+ envFrom:
+ - configMapRef:
+ name: tb-node-db-config
+ volumeMounts:
+ - mountPath: /config
+ name: tb-node-config
+ livenessProbe:
+ httpGet:
+ path: /login
+ port: http
+ initialDelaySeconds: 120
+ timeoutSeconds: 10
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-node
+ namespace: thingsboard
+spec:
+ type: ClusterIP
+ selector:
+ app: tb-node
+ ports:
+ - port: 8080
+ name: http
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-mqtt-transport
+ namespace: thingsboard
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: tb-mqtt-transport
+ template:
+ metadata:
+ labels:
+ app: tb-mqtt-transport
+ spec:
+ volumes:
+ - name: tb-mqtt-transport-config
+ configMap:
+ name: tb-mqtt-transport-config
+ items:
+ - key: conf
+ path: tb-mqtt-transport.conf
+ - key: logback
+ path: logback.xml
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: thingsboard/tb-mqtt-transport:latest
+ ports:
+ - containerPort: 1883
+ name: mqtt
+ env:
+ - name: CLUSTER_NODE_ID
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: TB_HOST
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: MQTT_BIND_ADDRESS
+ value: "0.0.0.0"
+ - name: MQTT_BIND_PORT
+ value: "1883"
+ - name: MQTT_TIMEOUT
+ value: "10000"
+ - name: TB_KAFKA_SERVERS
+ value: "tb-kafka:9092"
+ volumeMounts:
+ - mountPath: /config
+ name: tb-mqtt-transport-config
+ readinessProbe:
+ periodSeconds: 20
+ tcpSocket:
+ port: 1883
+ livenessProbe:
+ periodSeconds: 20
+ tcpSocket:
+ port: 1883
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-mqtt-transport
+ namespace: thingsboard
+spec:
+ type: LoadBalancer
+ selector:
+ app: tb-mqtt-transport
+ ports:
+ - port: 1883
+ targetPort: 1883
+ name: mqtt
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-http-transport
+ namespace: thingsboard
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: tb-http-transport
+ template:
+ metadata:
+ labels:
+ app: tb-http-transport
+ spec:
+ volumes:
+ - name: tb-http-transport-config
+ configMap:
+ name: tb-http-transport-config
+ items:
+ - key: conf
+ path: tb-http-transport.conf
+ - key: logback
+ path: logback.xml
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: thingsboard/tb-http-transport:latest
+ ports:
+ - containerPort: 8080
+ name: http
+ env:
+ - name: CLUSTER_NODE_ID
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: TB_HOST
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: HTTP_BIND_ADDRESS
+ value: "0.0.0.0"
+ - name: HTTP_BIND_PORT
+ value: "8080"
+ - name: HTTP_REQUEST_TIMEOUT
+ value: "60000"
+ - name: TB_KAFKA_SERVERS
+ value: "tb-kafka:9092"
+ volumeMounts:
+ - mountPath: /config
+ name: tb-http-transport-config
+ readinessProbe:
+ periodSeconds: 20
+ tcpSocket:
+ port: 8080
+ livenessProbe:
+ periodSeconds: 20
+ tcpSocket:
+ port: 8080
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-http-transport
+ namespace: thingsboard
+spec:
+ type: ClusterIP
+ selector:
+ app: tb-http-transport
+ ports:
+ - port: 8080
+ name: http
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-coap-transport
+ namespace: thingsboard
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: tb-coap-transport
+ template:
+ metadata:
+ labels:
+ app: tb-coap-transport
+ spec:
+ volumes:
+ - name: tb-coap-transport-config
+ configMap:
+ name: tb-coap-transport-config
+ items:
+ - key: conf
+ path: tb-coap-transport.conf
+ - key: logback
+ path: logback.xml
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: thingsboard/tb-coap-transport:latest
+ ports:
+ - containerPort: 5683
+ name: coap
+ protocol: UDP
+ env:
+ - name: CLUSTER_NODE_ID
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: TB_HOST
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: COAP_BIND_ADDRESS
+ value: "0.0.0.0"
+ - name: COAP_BIND_PORT
+ value: "5683"
+ - name: COAP_TIMEOUT
+ value: "10000"
+ - name: TB_KAFKA_SERVERS
+ value: "tb-kafka:9092"
+ volumeMounts:
+ - mountPath: /config
+ name: tb-coap-transport-config
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-coap-transport
+ namespace: thingsboard
+spec:
+ type: LoadBalancer
+ selector:
+ app: tb-coap-transport
+ ports:
+ - port: 5683
+ name: coap
+ protocol: UDP
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: tb-web-ui
+ namespace: thingsboard
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: tb-web-ui
+ template:
+ metadata:
+ labels:
+ app: tb-web-ui
+ spec:
+ containers:
+ - name: server
+ imagePullPolicy: Always
+ image: thingsboard/tb-web-ui:latest
+ ports:
+ - containerPort: 8080
+ name: http
+ env:
+ - name: HTTP_BIND_ADDRESS
+ value: "0.0.0.0"
+ - name: HTTP_BIND_PORT
+ value: "8080"
+ - name: TB_ENABLE_PROXY
+ value: "false"
+ - name: LOGGER_LEVEL
+ value: "info"
+ - name: LOG_FOLDER
+ value: "logs"
+ - name: LOGGER_FILENAME
+ value: "tb-web-ui-%DATE%.log"
+ - name: DOCKER_MODE
+ value: "true"
+ livenessProbe:
+ httpGet:
+ path: /index.html
+ port: http
+ initialDelaySeconds: 120
+ timeoutSeconds: 10
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tb-web-ui
+ namespace: thingsboard
+spec:
+ type: ClusterIP
+ selector:
+ app: tb-web-ui
+ ports:
+ - port: 8080
+ name: http
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+ name: tb-ingress
+ namespace: thingsboard
+ annotations:
+ nginx.ingress.kubernetes.io/use-regex: "true"
+ nginx.ingress.kubernetes.io/ssl-redirect: "false"
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+spec:
+ rules:
+ - http:
+ paths:
+ - path: /api/v1/.*
+ backend:
+ serviceName: tb-http-transport
+ servicePort: 8080
+ - path: /static/rulenode/.*
+ backend:
+ serviceName: tb-node
+ servicePort: 8080
+ - path: /static/.*
+ backend:
+ serviceName: tb-web-ui
+ servicePort: 8080
+ - path: /index.html.*
+ backend:
+ serviceName: tb-web-ui
+ servicePort: 8080
+ - path: /
+ backend:
+ serviceName: tb-web-ui
+ servicePort: 8080
+ - path: /.*
+ backend:
+ serviceName: tb-node
+ servicePort: 8080
+---
\ No newline at end of file
diff --git a/msa/js-executor/package-lock.json b/msa/js-executor/package-lock.json
index 1af16309a7..a01d4a9053 100644
--- a/msa/js-executor/package-lock.json
+++ b/msa/js-executor/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "thingsboard-js-executor",
- "version": "2.3.0",
+ "version": "2.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/msa/web-ui/package-lock.json b/msa/web-ui/package-lock.json
index c50ee7d073..16d10f5961 100644
--- a/msa/web-ui/package-lock.json
+++ b/msa/web-ui/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "thingsboard-web-ui",
- "version": "2.3.0",
+ "version": "2.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/pom.xml b/pom.xml
index f2cde239b7..b0bf2a51c3 100755
--- a/pom.xml
+++ b/pom.xml
@@ -29,10 +29,10 @@
${basedir}
- 1.4.3.RELEASE
- 4.3.4.RELEASE
- 4.2.0.RELEASE
- 1.8.10.RELEASE
+ 2.1.3.RELEASE
+ 5.1.5.RELEASE
+ 5.1.4.RELEASE
+ 2.1.5.RELEASE
2.9.0
0.7.0
2.2.0
@@ -41,8 +41,8 @@
1.2.3
1.9.5
0.10
- 3.5.0
- 3.3.0.2
+ 3.6.0
+ 3.5.0.1
1.2.7
21.0
2.6.1
@@ -50,7 +50,7 @@
1.5.0
2.5
1.4
- 2.8.11.1
+ 2.9.7
2.2.6
2.11
2.4.2
@@ -60,11 +60,11 @@
2.0
1.4.3
4.0.1
- 3.0.2
- 1.12.0
+ 3.6.1
+ 1.16.1
1.16.18
1.1.0
- 4.1.22.Final
+ 4.1.30.Final
1.5.0
4.8.0
2.19.1
@@ -85,6 +85,8 @@
2.0.0
4.1.1
2.57
+ 2.7.7
+ 1.23
@@ -512,6 +514,16 @@
+
+ org.yaml
+ snakeyaml
+ ${snakeyaml.version}
+
+
+ antlr
+ antlr
+ ${antlr.version}
+
com.rabbitmq
amqp-client
@@ -600,6 +612,16 @@
jackson-databind
${jackson.version}
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+
com.github.fge
json-schema-validator
@@ -794,6 +816,12 @@
de.ruedigermoeller
fst
${fst.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
io.springfox.ui
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 9b8224e0ff..39846f4c4e 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -468,9 +468,9 @@
"integrity": "sha512-o+V/OzwNGpS30QmgP7DJWTdBJ2BMDut481qqB72sM0L59dkO6TNjRV7qubQCntGqGe98h9vObweQUVYTfEO4vg=="
},
"angular-material": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/angular-material/-/angular-material-1.1.9.tgz",
- "integrity": "sha512-kxyigi+7823k/31qQ0j6wL5FkCe/mw2bAg1kfEFzIvhUoe5Myr+0YoQyN8D8EGaaOyolXU/VPtxgKSfOCSLEBw=="
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/angular-material/-/angular-material-1.1.13.tgz",
+ "integrity": "sha512-qWc5WOhRa/sbQmiRwenOla2Pky3w+wgW0l5Wp3J6jmB/WWxMWW7+JMdCXo1diGEETTKTF2vLdeWTceDTNehmSw=="
},
"angular-material-data-table": {
"version": "0.10.10",
@@ -7675,6 +7675,22 @@
}
}
},
+ "jstree": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/jstree/-/jstree-3.3.7.tgz",
+ "integrity": "sha512-yzzalO1TbZ4HdPezO43LesGI4Wv2sB0Nl+4GfwO0YYvehGws5qtTAhlBISxfur9phMLwCtf9GjHlRx2ZLXyRnw==",
+ "requires": {
+ "jquery": ">=1.9.1"
+ }
+ },
+ "jstree-bootstrap-theme": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/jstree-bootstrap-theme/-/jstree-bootstrap-theme-1.0.1.tgz",
+ "integrity": "sha1-fV7cc6hG6Np/lPV6HMXd7p2eq0s=",
+ "requires": {
+ "jquery": ">=1.9.1"
+ }
+ },
"keycode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
diff --git a/ui/package.json b/ui/package.json
index 2600c62438..0a1f4512a1 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -27,7 +27,7 @@
"angular-gridster": "^0.13.14",
"angular-hotkeys": "^1.7.0",
"angular-jwt": "^0.1.6",
- "angular-material": "1.1.9",
+ "angular-material": "1.1.13",
"angular-material-data-table": "^0.10.9",
"angular-material-expansion-panel": "^0.7.2",
"angular-material-icons": "^0.7.1",
@@ -60,6 +60,8 @@
"jquery.terminal": "^1.5.0",
"js-beautify": "^1.6.4",
"json-schema-defaults": "^0.2.0",
+ "jstree": "^3.3.7",
+ "jstree-bootstrap-theme": "^1.0.1",
"leaflet": "^1.0.3",
"leaflet-providers": "^1.1.17",
"material-ui": "^0.16.1",
diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js
index ad5dc63da4..9294ca39d4 100644
--- a/ui/src/app/api/entity-relation.service.js
+++ b/ui/src/app/api/entity-relation.service.js
@@ -164,13 +164,13 @@ function EntityRelationService($http, $q) {
return deferred.promise;
}
- function findByQuery(query) {
+ function findByQuery(query, config) {
var deferred = $q.defer();
var url = '/api/relations';
- $http.post(url, query).then(function success(response) {
+ $http.post(url, query, config).then(function success(response) {
deferred.resolve(response.data);
- }, function fail() {
- deferred.reject();
+ }, function fail(e) {
+ deferred.reject(e);
});
return deferred.promise;
}
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 3349558204..ffcdc5f896 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -362,6 +362,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
$location.search('publicId', null);
deferred.reject();
});
+ } else if (locationSearch.accessToken) {
+ var token = locationSearch.accessToken;
+ var refreshToken = locationSearch.refreshToken;
+ $location.search('accessToken', null);
+ if (refreshToken) {
+ $location.search('refreshToken', null);
+ }
+ try {
+ updateAndValidateToken(token, 'jwt_token', false);
+ if (refreshToken) {
+ updateAndValidateToken(refreshToken, 'refresh_token', false);
+ } else {
+ store.remove('refresh_token');
+ store.remove('refresh_token_expiration');
+ }
+ } catch (e) {
+ deferred.reject();
+ }
+ procceedJwtTokenValidate();
} else {
procceedJwtTokenValidate();
}
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index a32d2b0f5c..70cf76ff1a 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
+import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget';
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@@ -32,6 +33,7 @@ import TbAnalogueCompass from '../widget/lib/analogue-compass';
import TbCanvasDigitalGauge from '../widget/lib/canvas-digital-gauge';
import TbMapWidget from '../widget/lib/map-widget';
import TbMapWidgetV2 from '../widget/lib/map-widget2';
+import TripAnimationWidget from '../widget/lib/tripAnimation/trip-animation-widget';
import 'jquery.terminal/js/jquery.terminal.min.js';
import 'jquery.terminal/css/jquery.terminal.min.css';
@@ -43,7 +45,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
- thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
+ thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
.factory('widgetService', WidgetService)
.name;
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index dbef8a9c6b..7081415ff5 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -52,7 +52,8 @@ import 'react-schema-form';
import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import 'ngFlowchart/dist/ngFlowchart';
-
+import 'jstree/dist/jstree.min';
+import 'jstree-bootstrap-theme/dist/themes/proton/style.min.css';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
import 'angular-material/angular-material.min.css';
diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js
index 1b460b6238..80f8a6cdda 100644
--- a/ui/src/app/components/json-form.directive.js
+++ b/ui/src/app/components/json-form.directive.js
@@ -71,7 +71,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
$compile(element.contents())(childScope);
}
+ scope.isFullscreen = false;
+
scope.formProps = {
+ isFullscreen: false,
option: {
formDefaults: {
startEmpty: true
@@ -86,6 +89,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
},
onColorClick: function(event, key, val) {
scope.showColorPicker(event, val);
+ },
+ onToggleFullscreen: function() {
+ scope.isFullscreen = !scope.isFullscreen;
+ scope.formProps.isFullscreen = scope.isFullscreen;
}
};
@@ -116,6 +123,8 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
});
}
+ scope.onFullscreenChanged = function() {}
+
scope.validate = function(){
if (scope.schema && scope.model) {
var result = utils.validateBySchema(scope.schema, scope.model);
diff --git a/ui/src/app/components/json-form.tpl.html b/ui/src/app/components/json-form.tpl.html
index c15f3f5eb1..f5096c25b7 100644
--- a/ui/src/app/components/json-form.tpl.html
+++ b/ui/src/app/components/json-form.tpl.html
@@ -15,4 +15,6 @@
limitations under the License.
-->
-
\ No newline at end of file
+
+
+
diff --git a/ui/src/app/components/nav-tree.directive.js b/ui/src/app/components/nav-tree.directive.js
new file mode 100644
index 0000000000..119ef98529
--- /dev/null
+++ b/ui/src/app/components/nav-tree.directive.js
@@ -0,0 +1,206 @@
+/*
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './nav-tree.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import navTreeTemplate from './nav-tree.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.navTree', [])
+ .directive('tbNavTree', NavTree)
+ .name;
+
+/*@ngInject*/
+function NavTree() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ loadNodes: '=',
+ editCallbacks: '=',
+ enableSearch: '@?',
+ onNodeSelected: '&',
+ onNodesInserted: '&',
+ searchCallback: '&?'
+ },
+ controller: NavTreeController,
+ controllerAs: 'vm',
+ templateUrl: navTreeTemplate
+ };
+}
+
+/*@ngInject*/
+function NavTreeController($scope, $element, types) {
+
+ var vm = this;
+ vm.types = types;
+
+ $scope.$watch('vm.loadNodes', (newVal) => {
+ if (newVal) {
+ initTree();
+ }
+ });
+
+ function initTree() {
+ var config = {
+ core: {
+ multiple: false,
+ check_callback: true,
+ themes: { name: 'proton', responsive: true },
+ data: vm.loadNodes
+ }
+ };
+
+ if (vm.enableSearch) {
+ config.plugins = ["search"];
+ config.search = {
+ case_sensitive: false,
+ show_only_matches: true,
+ show_only_matches_children: false,
+ search_leaves_only: false
+ };
+ if (vm.searchCallback) {
+ config.search.search_callback = (searchText, node) => vm.searchCallback({searchText: searchText, node: node});
+ }
+ }
+
+ vm.treeElement = angular.element('.tb-nav-tree-container', $element)
+ .jstree(config);
+
+ vm.treeElement.on("changed.jstree", function (e, data) {
+ if (vm.onNodeSelected) {
+ vm.onNodeSelected({node: data.instance.get_selected(true)[0], event: e});
+ }
+ });
+
+ vm.treeElement.on("model.jstree", function (e, data) {
+ if (vm.onNodesInserted) {
+ vm.onNodesInserted({nodes: data.nodes, parent: data.parent});
+ }
+ });
+
+ if (vm.editCallbacks) {
+ vm.editCallbacks.selectNode = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ vm.treeElement.jstree('deselect_all', true);
+ vm.treeElement.jstree('select_node', node);
+ }
+ };
+ vm.editCallbacks.deselectAll = () => {
+ vm.treeElement.jstree('deselect_all');
+ };
+ vm.editCallbacks.getNode = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ return node;
+ };
+ vm.editCallbacks.getParentNodeId = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ return vm.treeElement.jstree('get_parent', node);
+ }
+ };
+ vm.editCallbacks.openNode = (id, cb) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ vm.treeElement.jstree('open_node', node, cb);
+ }
+ };
+ vm.editCallbacks.nodeIsOpen = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ return vm.treeElement.jstree('is_open', node);
+ } else {
+ return true;
+ }
+ };
+ vm.editCallbacks.nodeIsLoaded = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ return vm.treeElement.jstree('is_loaded', node);
+ } else {
+ return true;
+ }
+ };
+ vm.editCallbacks.refreshNode = (id) => {
+ if (id === '#') {
+ vm.treeElement.jstree('refresh');
+ vm.treeElement.jstree('redraw');
+ } else {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ var opened = vm.treeElement.jstree('is_open', node);
+ vm.treeElement.jstree('refresh_node', node);
+ vm.treeElement.jstree('redraw');
+ if (node.children && opened/* && !node.children.length*/) {
+ vm.treeElement.jstree('open_node', node);
+ }
+ }
+ }
+ };
+ vm.editCallbacks.updateNode = (id, newName) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ vm.treeElement.jstree('rename_node', node, newName);
+ }
+ };
+ vm.editCallbacks.createNode = (parentId, node, pos) => {
+ var parentNode = vm.treeElement.jstree('get_node', parentId);
+ if (parentNode) {
+ vm.treeElement.jstree('create_node', parentNode, node, pos);
+ }
+ };
+ vm.editCallbacks.deleteNode = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ vm.treeElement.jstree('delete_node', node);
+ }
+ };
+ vm.editCallbacks.disableNode = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ vm.treeElement.jstree('disable_node', node);
+ }
+ };
+ vm.editCallbacks.enableNode = (id) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ vm.treeElement.jstree('enable_node', node);
+ }
+ };
+ vm.editCallbacks.setNodeHasChildren = (id, hasChildren) => {
+ var node = vm.treeElement.jstree('get_node', id);
+ if (node) {
+ if (!node.children || !node.children.length) {
+ node.children = hasChildren;
+ node.state.loaded = !hasChildren;
+ node.state.opened = false;
+ vm.treeElement.jstree('_node_changed', node.id);
+ vm.treeElement.jstree('redraw');
+ }
+ }
+ };
+ vm.editCallbacks.search = (searchText) => {
+ vm.treeElement.jstree('search', searchText);
+ };
+ vm.editCallbacks.clearSearch = () => {
+ vm.treeElement.jstree('clear_search');
+ };
+ }
+ }
+}
diff --git a/ui/src/app/components/nav-tree.scss b/ui/src/app/components/nav-tree.scss
new file mode 100644
index 0000000000..4a9ac4739b
--- /dev/null
+++ b/ui/src/app/components/nav-tree.scss
@@ -0,0 +1,346 @@
+/**
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-nav-tree-container {
+ padding: 15px;
+ font-family: Roboto, "Helvetica Neue", sans-serif;
+
+ &.jstree-proton {
+ .jstree-node,
+ .jstree-icon {
+ background-image: url("../../png/jstree/32px.png");
+ }
+
+ .jstree-last {
+ background: transparent;
+ }
+
+ .jstree-themeicon-custom {
+ background-image: none;
+
+ &.material-icons {
+ font-size: 18px;
+ }
+ }
+
+ .jstree-anchor {
+ font-size: 16px;
+ }
+ }
+
+ &.jstree-proton-small {
+ .jstree-node,
+ .jstree-icon {
+ background-image: url("../../png/jstree/32px.png");
+ }
+
+ .jstree-last {
+ background: transparent;
+ }
+
+ .jstree-themeicon-custom {
+ background-image: none;
+
+ &.material-icons {
+ font-size: 14px;
+ }
+ }
+
+ .jstree-anchor {
+ font-size: 14px;
+ }
+ }
+
+ &.jstree-proton-large {
+ .jstree-node,
+ .jstree-icon {
+ background-image: url("../../png/jstree/32px.png");
+ }
+
+ .jstree-last {
+ background: transparent;
+ }
+
+ .jstree-themeicon-custom {
+ background-image: none;
+
+ &.material-icons {
+ font-size: 24px;
+ }
+ }
+
+ .jstree-anchor {
+ font-size: 20px;
+ }
+ }
+
+ a {
+ border-bottom: none;
+
+ i.jstree-themeicon-custom {
+ &.tb-user-group {
+ &::before {
+ content: "account_circle";
+ }
+ }
+
+ &.tb-customer-group {
+ &::before {
+ content: "supervisor_account";
+ }
+ }
+
+ &.tb-asset-group {
+ &::before {
+ content: "domain";
+ }
+ }
+
+ &.tb-device-group {
+ &::before {
+ content: "devices_other";
+ }
+ }
+
+ &.tb-entity-view-group {
+ &::before {
+ content: "view_quilt";
+ }
+ }
+
+ &.tb-dashboard-group {
+ &::before {
+ content: "dashboard";
+ }
+ }
+
+ &.tb-customer {
+ &::before {
+ content: "supervisor_account";
+ }
+ }
+ }
+ }
+}
+
+@media (max-width: 768px) {
+ .tb-nav-tree-container {
+ &.jstree-proton-responsive {
+ .jstree-node,
+ .jstree-icon,
+ .jstree-node > .jstree-ocl,
+ .jstree-themeicon,
+ .jstree-checkbox {
+ background-image: url("../../png/jstree/40px.png");
+ background-size: 120px 240px;
+ }
+
+ .jstree-container-ul {
+ overflow: visible;
+ }
+
+ .jstree-themeicon-custom {
+ background-color: transparent;
+ background-image: none;
+ background-position: 0 0;
+
+ &.material-icons {
+ margin: 0;
+ font-size: 24px;
+ }
+ }
+
+ .jstree-node,
+ .jstree-leaf > .jstree-ocl {
+ background: 0 0;
+ }
+
+ .jstree-node {
+ min-width: 40px;
+ min-height: 40px;
+ margin-left: 40px;
+ line-height: 40px;
+ white-space: nowrap;
+ background-repeat: repeat-y;
+ background-position: -80px 0;
+ }
+
+ .jstree-last {
+ background: 0 0;
+ }
+
+ .jstree-anchor {
+ height: 40px;
+ font-size: 1.1em;
+ font-weight: 700;
+ line-height: 40px;
+ text-shadow: 1px 1px #fff;
+ }
+
+ .jstree-icon,
+ .jstree-icon:empty {
+ width: 40px;
+ height: 40px;
+ line-height: 40px;
+ }
+
+ > {
+ .jstree-container-ul > .jstree-node {
+ margin-right: 0;
+ margin-left: 0;
+ }
+ }
+
+ .jstree-ocl,
+ .jstree-themeicon,
+ .jstree-checkbox {
+ background-size: 120px 240px;
+ }
+
+ .jstree-leaf > .jstree-ocl {
+ background: 0 0;
+ background-position: -40px -120px;
+ }
+
+ .jstree-last > .jstree-ocl {
+ background-position: -40px -160px;
+ }
+
+ .jstree-open > .jstree-ocl {
+ background-position: 0 0 !important;
+ }
+
+ .jstree-closed > .jstree-ocl {
+ background-position: 0 -40px !important;
+ }
+
+ .jstree-themeicon {
+ background-position: -40px -40px;
+ }
+
+ .jstree-checkbox,
+ .jstree-checkbox:hover {
+ background-position: -40px -80px;
+ }
+
+ &.jstree-checkbox-selection {
+ .jstree-clicked > .jstree-checkbox,
+ .jstree-clicked > .jstree-checkbox:hover {
+ background-position: 0 -80px;
+ }
+ }
+
+ .jstree-checked > .jstree-checkbox,
+ .jstree-checked > .jstree-checkbox:hover {
+ background-position: 0 -80px;
+ }
+
+ .jstree-anchor > .jstree-undetermined,
+ .jstree-anchor > .jstree-undetermined:hover {
+ background-position: 0 -120px;
+ }
+
+ .jstree-striped {
+ background: 0 0;
+ }
+
+ .jstree-wholerow {
+ height: 40px;
+ background: #ebebeb;
+ border-top: 1px solid rgba(255, 255, 255, .7);
+ border-bottom: 1px solid rgba(64, 64, 64, .2);
+ }
+
+ .jstree-wholerow-hovered {
+ background: #e7f4f9;
+ }
+
+ .jstree-wholerow-clicked {
+ background: #beebff;
+ }
+
+ .jstree-children {
+ .jstree-last > .jstree-wholerow {
+ box-shadow: inset 0 -6px 3px -5px #666;
+ }
+
+ .jstree-open > .jstree-wholerow {
+ border-top: 0;
+ box-shadow: inset 0 6px 3px -5px #666;
+ }
+
+ .jstree-open + .jstree-open {
+ box-shadow: none;
+ }
+ }
+
+ &.jstree-rtl {
+ .jstree-node {
+ margin-right: 40px;
+ margin-left: 0;
+ }
+
+ .jstree-container-ul > .jstree-node {
+ margin-right: 0;
+ }
+
+ .jstree-closed > .jstree-ocl {
+ background-position: -40px 0 !important;
+ }
+ }
+ }
+ }
+}
+
+.tb-nav-tree .md-button.tb-active {
+ font-weight: 500;
+ background-color: rgba(255, 255, 255, .15);
+}
+
+.tb-nav-tree,
+.tb-nav-tree ul {
+ margin-top: 0;
+ list-style: none;
+
+ &:first-child {
+ padding: 0;
+ }
+
+ li {
+ .md-button {
+ width: 100%;
+ max-height: 40px;
+ padding: 0 16px;
+ margin: 0;
+ overflow: hidden;
+ line-height: 40px;
+ color: inherit;
+ text-align: left;
+ text-decoration: none;
+ text-overflow: ellipsis;
+ text-transform: none;
+ text-rendering: optimizeLegibility;
+ white-space: nowrap;
+ cursor: pointer;
+ border-radius: 0;
+
+ span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/components/nav-tree.tpl.html b/ui/src/app/components/nav-tree.tpl.html
new file mode 100644
index 0000000000..88612965ff
--- /dev/null
+++ b/ui/src/app/components/nav-tree.tpl.html
@@ -0,0 +1,18 @@
+
+
diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx
index a1f576bc43..44df6cd38d 100644
--- a/ui/src/app/components/react/json-form-ace-editor.jsx
+++ b/ui/src/app/components/react/json-form-ace-editor.jsx
@@ -34,8 +34,10 @@ class ThingsboardAceEditor extends React.Component {
this.onFocus = this.onFocus.bind(this);
this.onTidy = this.onTidy.bind(this);
this.onLoad = this.onLoad.bind(this);
+ this.onToggleFull = this.onToggleFull.bind(this);
var value = props.value ? props.value + '' : '';
this.state = {
+ isFull: false,
value: value,
focused: false
};
@@ -76,9 +78,26 @@ class ThingsboardAceEditor extends React.Component {
}
onLoad(editor) {
+ this.aceEditor = editor;
fixAceEditor(editor);
}
+ onToggleFull() {
+ this.setState({ isFull: !this.state.isFull });
+ this.props.onToggleFullscreen();
+ this.updateAceEditorSize = true;
+ }
+
+ componentDidUpdate() {
+ if (this.updateAceEditorSize) {
+ if (this.aceEditor) {
+ this.aceEditor.resize();
+ this.aceEditor.renderer.updateFull();
+ }
+ this.updateAceEditorSize = false;
+ }
+ }
+
render() {
const styles = reactCSS({
@@ -108,18 +127,23 @@ class ThingsboardAceEditor extends React.Component {
if (this.state.focused) {
labelClass += " tb-focused";
}
-
+ var containerClass = "tb-container";
+ var style = this.props.form.style || {width: '100%'};
+ if (this.state.isFull) {
+ containerClass += " fullscreen-form-field";
+ }
return (
-
+
{this.props.form.title}
{this.props.mode}
+
+ style={style}/>
{this.props.error}
+ style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}
);
}
diff --git a/ui/src/app/components/react/json-form-ace-editor.scss b/ui/src/app/components/react/json-form-ace-editor.scss
index 3ae4530b9c..fa32b74a07 100644
--- a/ui/src/app/components/react/json-form-ace-editor.scss
+++ b/ui/src/app/components/react/json-form-ace-editor.scss
@@ -13,6 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+.fullscreen-form-field {
+ .json-form-ace-editor {
+ height: calc(100% - 60px);
+ }
+}
+
.json-form-ace-editor {
position: relative;
height: 100%;
diff --git a/ui/src/app/components/react/json-form-array.jsx b/ui/src/app/components/react/json-form-array.jsx
index b42bac55a8..46f6457b0f 100644
--- a/ui/src/app/components/react/json-form-array.jsx
+++ b/ui/src/app/components/react/json-form-array.jsx
@@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component {
}
let forms = this.props.form.items.map(function(form, index){
var copy = this.copyWithIndex(form, i);
- return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder);
+ return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this));
arrays.push(
diff --git a/ui/src/app/components/react/json-form-fieldset.jsx b/ui/src/app/components/react/json-form-fieldset.jsx
index 8434025c7b..5a0f94017c 100644
--- a/ui/src/app/components/react/json-form-fieldset.jsx
+++ b/ui/src/app/components/react/json-form-fieldset.jsx
@@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component {
render() {
let forms = this.props.form.items.map(function(form, index){
- return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder);
+ return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this));
return (
diff --git a/ui/src/app/components/react/json-form-react.jsx b/ui/src/app/components/react/json-form-react.jsx
index 59d007532c..18b2de26ef 100644
--- a/ui/src/app/components/react/json-form-react.jsx
+++ b/ui/src/app/components/react/json-form-react.jsx
@@ -50,7 +50,8 @@ ReactSchemaForm.propTypes = {
model: React.PropTypes.object,
option: React.PropTypes.object,
onModelChange: React.PropTypes.func,
- onColorClick: React.PropTypes.func
+ onColorClick: React.PropTypes.func,
+ onToggleFullscreen: React.PropTypes.func
}
ReactSchemaForm.defaultProps = {
diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx
index 4c3aefe66e..47d20426b7 100644
--- a/ui/src/app/components/react/json-form-schema-form.jsx
+++ b/ui/src/app/components/react/json-form-schema-form.jsx
@@ -63,6 +63,7 @@ class ThingsboardSchemaForm extends React.Component {
this.onChange = this.onChange.bind(this);
this.onColorClick = this.onColorClick.bind(this);
+ this.onToggleFullscreen = this.onToggleFullscreen.bind(this);
this.hasConditions = false;
}
@@ -78,7 +79,11 @@ class ThingsboardSchemaForm extends React.Component {
this.props.onColorClick(event, key, val);
}
- builder(form, model, index, onChange, onColorClick, mapper) {
+ onToggleFullscreen() {
+ this.props.onToggleFullscreen();
+ }
+
+ builder(form, model, index, onChange, onColorClick, onToggleFullscreen, mapper) {
var type = form.type;
let Field = this.mapper[type];
if(!Field) {
@@ -91,7 +96,7 @@ class ThingsboardSchemaForm extends React.Component {
return null;
}
}
- return
+ return
}
render() {
@@ -101,11 +106,16 @@ class ThingsboardSchemaForm extends React.Component {
mapper = _.merge(this.mapper, this.props.mapper);
}
let forms = merged.map(function(form, index) {
- return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, mapper);
+ return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper);
}.bind(this));
+ let formClass = 'SchemaForm';
+ if (this.props.isFullscreen) {
+ formClass += ' SchemaFormFullscreen';
+ }
+
return (
- {forms}
+ {forms}
);
}
}
diff --git a/ui/src/app/components/react/json-form.scss b/ui/src/app/components/react/json-form.scss
index eabf2725ef..d36ec5c0e8 100644
--- a/ui/src/app/components/react/json-form.scss
+++ b/ui/src/app/components/react/json-form.scss
@@ -21,6 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
$input-label-float-offset: 6px !default;
$input-label-float-scale: .75 !default;
+.SchemaForm {
+ &.SchemaFormFullscreen {
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ > div:not(.fullscreen-form-field) {
+ display: none;
+ }
+
+ > div.fullscreen-form-field {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ }
+ }
+}
+
.json-form-error {
position: relative;
bottom: -5px;
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index bb450490a8..c0ef817a3b 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -27,6 +27,7 @@ import thingsboardApiUser from '../api/user.service';
import thingsboardNoAnimate from '../components/no-animate.directive';
import thingsboardOnFinishRender from '../components/finish-render.directive';
import thingsboardSideMenu from '../components/side-menu.directive';
+import thingsboardNavTree from '../components/nav-tree.directive';
import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
import thingsboardKvMap from '../components/kv-map.directive';
import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
@@ -89,6 +90,7 @@ export default angular.module('thingsboard.home', [
thingsboardNoAnimate,
thingsboardOnFinishRender,
thingsboardSideMenu,
+ thingsboardNavTree,
thingsboardDashboardAutocomplete,
thingsboardKvMap,
thingsboardJsonObjectEdit,
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 9e38c55aaa..dfef502f09 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/ui/src/app/locale/locale.constant-en_US.json
@@ -1566,7 +1566,8 @@
"row-click": "On row click",
"polygon-click": "On polygon click",
"marker-click": "On marker click",
- "tooltip-tag-action": "Tooltip tag action"
+ "tooltip-tag-action": "Tooltip tag action",
+ "node-selected": "On node selected"
}
},
"language": {
diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js
index a3e76a9c9b..21d80ec327 100644
--- a/ui/src/app/locale/translate-handler.js
+++ b/ui/src/app/locale/translate-handler.js
@@ -18,12 +18,12 @@
.name;
/*@ngInject*/
-function ThingsboardMissingTranslateHandler($log, types) {
+function ThingsboardMissingTranslateHandler(/*$log, types*/) {
- return function (translationId) {
- if (translationId && !translationId.startsWith(types.translate.customTranslationsPrefix)) {
+ return function (/*translationId*/) {
+ /*if (translationId && !translationId.startsWith(types.translate.customTranslationsPrefix)) {
$log.warn('Translation for ' + translationId + ' doesn\'t exist');
- }
+ }*/
};
}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.js b/ui/src/app/widget/lib/entities-hierarchy-widget.js
new file mode 100644
index 0000000000..93b3912709
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-hierarchy-widget.js
@@ -0,0 +1,530 @@
+/*
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './entities-hierarchy-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitiesHierarchyWidgetTemplate from './entities-hierarchy-widget.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.widgets.entitiesHierarchyWidget', [])
+ .directive('tbEntitiesHierarchyWidget', EntitiesHierarchyWidget)
+ .name;
+
+/*@ngInject*/
+function EntitiesHierarchyWidget() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ hierarchyId: '=',
+ ctx: '='
+ },
+ controller: EntitiesHierarchyWidgetController,
+ controllerAs: 'vm',
+ templateUrl: entitiesHierarchyWidgetTemplate
+ };
+}
+
+/*@ngInject*/
+function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast, types, entityService, entityRelationService /*$filter, $mdMedia, $mdPanel, $document, $translate, $timeout, utils, types*/) {
+ var vm = this;
+
+ vm.showData = true;
+
+ vm.nodeEditCallbacks = {};
+
+ vm.nodeIdCounter = 0;
+
+ vm.nodesMap = {};
+ vm.pendingUpdateNodeTasks = {};
+
+ vm.query = {
+ search: null
+ };
+
+ vm.searchAction = {
+ name: 'action.search',
+ show: true,
+ onAction: function() {
+ vm.enterFilterMode();
+ },
+ icon: 'search'
+ };
+
+ vm.onNodesInserted = onNodesInserted;
+ vm.onNodeSelected = onNodeSelected;
+ vm.enterFilterMode = enterFilterMode;
+ vm.exitFilterMode = exitFilterMode;
+ vm.searchCallback = searchCallback;
+
+ $scope.$watch('vm.ctx', function() {
+ if (vm.ctx && vm.ctx.defaultSubscription) {
+ vm.settings = vm.ctx.settings;
+ vm.widgetConfig = vm.ctx.widgetConfig;
+ vm.subscription = vm.ctx.defaultSubscription;
+ vm.datasources = vm.subscription.datasources;
+ initializeConfig();
+ updateDatasources();
+ }
+ });
+
+ $scope.$watch("vm.query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+ updateSearchNodes();
+ }
+ });
+
+ $scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) {
+ if (vm.hierarchyId == hierarchyId) {
+ if (vm.subscription) {
+ updateNodeData(vm.subscription.data);
+ }
+ }
+ });
+
+ function initializeConfig() {
+
+ vm.ctx.widgetActions = [ vm.searchAction ];
+
+ var testNodeCtx = {
+ entity: {
+ id: {
+ entityType: 'DEVICE',
+ id: '123'
+ },
+ name: 'TEST DEV1'
+ },
+ data: {},
+ level: 2
+ };
+ var parentNodeCtx = angular.copy(testNodeCtx);
+ parentNodeCtx.level = 1;
+ testNodeCtx.parentNodeCtx = parentNodeCtx;
+
+ var nodeRelationQueryFunction = loadNodeCtxFunction(vm.settings.nodeRelationQueryFunction, 'nodeCtx', testNodeCtx);
+ var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx);
+ var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx);
+ var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx);
+ var nodeOpenedFunction = loadNodeCtxFunction(vm.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx);
+ var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx);
+
+ var testNodeCtx2 = angular.copy(testNodeCtx);
+ testNodeCtx2.entity.name = 'TEST DEV2';
+
+ var nodesSortFunction = loadNodeCtxFunction(vm.settings.nodesSortFunction, 'nodeCtx1,nodeCtx2', testNodeCtx, testNodeCtx2);
+
+ vm.nodeRelationQueryFunction = nodeRelationQueryFunction || defaultNodeRelationQueryFunction;
+ vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction;
+ vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name);
+ vm.nodeDisabledFunction = nodeDisabledFunction || (() => false);
+ vm.nodeOpenedFunction = nodeOpenedFunction || defaultNodeOpenedFunction;
+ vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true);
+ vm.nodesSortFunction = nodesSortFunction || defaultSortFunction;
+ }
+
+ function loadNodeCtxFunction(functionBody, argNames, ...args) {
+ var nodeCtxFunction = null;
+ if (angular.isDefined(functionBody) && functionBody.length) {
+ try {
+ nodeCtxFunction = new Function(argNames, functionBody);
+ var res = nodeCtxFunction.apply(null, args);
+ if (angular.isUndefined(res)) {
+ nodeCtxFunction = null;
+ }
+ } catch (e) {
+ nodeCtxFunction = null;
+ }
+ }
+ return nodeCtxFunction;
+ }
+
+ function enterFilterMode () {
+ vm.query.search = '';
+ vm.ctx.hideTitlePanel = true;
+ $timeout(()=>{
+ angular.element(vm.ctx.$container).find('.searchInput').focus();
+ })
+ }
+
+ function exitFilterMode () {
+ vm.query.search = null;
+ updateSearchNodes();
+ vm.ctx.hideTitlePanel = false;
+ }
+
+ function searchCallback (searchText, node) {
+ var theNode = vm.nodesMap[node.id];
+ if (theNode && theNode.data.searchText) {
+ return theNode.data.searchText.includes(searchText.toLowerCase());
+ }
+ return false;
+ }
+
+ function updateDatasources() {
+ vm.loadNodes = loadNodes;
+ }
+
+ function updateSearchNodes() {
+ if (vm.query.search != null) {
+ vm.nodeEditCallbacks.search(vm.query.search);
+ } else {
+ vm.nodeEditCallbacks.clearSearch();
+ }
+ }
+
+ function onNodesInserted(nodes/*, parent*/) {
+ if (nodes) {
+ nodes.forEach((nodeId) => {
+ var task = vm.pendingUpdateNodeTasks[nodeId];
+ if (task) {
+ task();
+ delete vm.pendingUpdateNodeTasks[nodeId];
+ }
+ });
+ }
+ }
+
+ function onNodeSelected(node, event) {
+ var nodeId;
+ if (!node) {
+ nodeId = -1;
+ } else {
+ nodeId = node.id;
+ }
+ if (nodeId !== -1) {
+ var selectedNode = vm.nodesMap[nodeId];
+ if (selectedNode) {
+ var descriptors = vm.ctx.actionsApi.getActionDescriptors('nodeSelected');
+ if (descriptors.length) {
+ var entity = selectedNode.data.nodeCtx.entity;
+ vm.ctx.actionsApi.handleWidgetAction(event, descriptors[0], entity.id, entity.name, { nodeCtx: selectedNode.data.nodeCtx });
+ }
+ }
+ }
+ }
+
+ function updateNodeData(subscriptionData) {
+ var affectedNodes = [];
+ if (subscriptionData) {
+ for (var i=0;i {
+ var node = vm.nodeEditCallbacks.getNode(nodeId);
+ if (node) {
+ updateNodeStyle(vm.nodesMap[nodeId]);
+ } else {
+ vm.pendingUpdateNodeTasks[nodeId] = () => {
+ updateNodeStyle(vm.nodesMap[nodeId]);
+ };
+ }
+ });
+ }
+
+ function updateNodeStyle(node) {
+ var newText = prepareNodeText(node);
+ if (!angular.equals(node.text, newText)) {
+ node.text = newText;
+ vm.nodeEditCallbacks.updateNode(node.id, node.text);
+ }
+ var newDisabled = vm.nodeDisabledFunction(node.data.nodeCtx);
+ if (!angular.equals(node.state.disabled, newDisabled)) {
+ node.state.disabled = newDisabled;
+ if (node.state.disabled) {
+ vm.nodeEditCallbacks.disableNode(node.id);
+ } else {
+ vm.nodeEditCallbacks.enableNode(node.id);
+ }
+ }
+ var newHasChildren = vm.nodeHasChildrenFunction(node.data.nodeCtx);
+ if (!angular.equals(node.children, newHasChildren)) {
+ node.children = newHasChildren;
+ vm.nodeEditCallbacks.setNodeHasChildren(node.id, node.children);
+ }
+ }
+
+ function prepareNodeText(node) {
+ var nodeIcon = prepareNodeIcon(node.data.nodeCtx);
+ var nodeText = vm.nodeTextFunction(node.data.nodeCtx);
+ node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : "";
+ return nodeIcon + nodeText;
+ }
+
+ function loadNodes(node, cb) {
+ if (node.id === '#') {
+ var tasks = [];
+ for (var i=0;i {
+ cb(prepareNodes(nodes));
+ updateNodeData(vm.subscription.data);
+ });
+ } else {
+ if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') {
+ var relationQuery = prepareNodeRelationQuery(node.data.nodeCtx);
+ entityRelationService.findByQuery(relationQuery, {ignoreErrors: true, ignoreLoading: true}).then(
+ (entityRelations) => {
+ var tasks = [];
+ for (var i=0;i {
+ cb(prepareNodes(nodes));
+ });
+ },
+ (error) => {
+ var errorText = "Failed to get relations!";
+ if (error && error.status === 400) {
+ errorText = "Invalid relations query returned by 'Node relations query function'! Please check widget configuration!";
+ }
+ showError(errorText);
+ }
+ );
+ } else {
+ cb([]);
+ }
+ }
+ }
+
+ function showError(errorText) {
+ var toastParent = angular.element('.tb-entities-hierarchy', $element);
+ toast.showError(errorText, toastParent, 'bottom left');
+ }
+
+ function prepareNodes(nodes) {
+ nodes = nodes.filter((node) => node !== null);
+ nodes.sort((node1, node2) => vm.nodesSortFunction(node1.data.nodeCtx, node2.data.nodeCtx));
+ return nodes;
+ }
+
+ function datasourceToNode(datasource, parentNodeCtx) {
+ var deferred = $q.defer();
+ resolveEntity(datasource).then(
+ (entity) => {
+ if (entity != null) {
+ var node = {
+ id: ++vm.nodeIdCounter
+ };
+ vm.nodesMap[node.id] = node;
+ datasource.nodeId = node.id;
+ node.icon = false;
+ var nodeCtx = {
+ parentNodeCtx: parentNodeCtx,
+ entity: entity,
+ data: {}
+ };
+ nodeCtx.level = parentNodeCtx ? parentNodeCtx.level + 1 : 1;
+ node.data = {
+ datasource: datasource,
+ nodeCtx: nodeCtx
+ };
+ node.state = {
+ disabled: vm.nodeDisabledFunction(node.data.nodeCtx),
+ opened: vm.nodeOpenedFunction(node.data.nodeCtx)
+ };
+ node.text = prepareNodeText(node);
+ node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx);
+ deferred.resolve(node);
+ } else {
+ deferred.resolve(null);
+ }
+ }
+ );
+ return deferred.promise;
+ }
+
+ function entityIdToNode(entityType, entityId, parentDatasource, parentNodeCtx) {
+ var deferred = $q.defer();
+ var datasource = {
+ dataKeys: parentDatasource.dataKeys,
+ type: types.datasourceType.entity,
+ entityType: entityType,
+ entityId: entityId
+ };
+ datasourceToNode(datasource, parentNodeCtx).then(
+ (node) => {
+ if (node != null) {
+ var subscriptionOptions = {
+ type: types.widgetType.latest.value,
+ datasources: [datasource],
+ callbacks: {
+ onDataUpdated: (subscription) => {
+ updateNodeData(subscription.data);
+ }
+ }
+ };
+ vm.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).then(
+ (/*subscription*/) => {
+ deferred.resolve(node);
+ }
+ );
+ } else {
+ deferred.resolve(node);
+ }
+ }
+ );
+ return deferred.promise;
+ }
+
+ function resolveEntity(datasource) {
+ var deferred = $q.defer();
+ if (datasource.type === types.datasourceType.function) {
+ var entity = {
+ id: {
+ entityType: "function"
+ },
+ name: datasource.name
+ }
+ deferred.resolve(entity);
+ } else {
+ entityService.getEntity(datasource.entityType, datasource.entityId, {ignoreLoading: true}).then(
+ (entity) => {
+ deferred.resolve(entity);
+ },
+ () => {
+ deferred.resolve(null);
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+
+ function prepareNodeRelationQuery(nodeCtx) {
+ var relationQuery = vm.nodeRelationQueryFunction(nodeCtx);
+ if (relationQuery && relationQuery === 'default') {
+ relationQuery = defaultNodeRelationQueryFunction(nodeCtx);
+ }
+ return relationQuery;
+ }
+
+ function defaultNodeRelationQueryFunction(nodeCtx) {
+ var entity = nodeCtx.entity;
+ var query = {
+ parameters: {
+ rootId: entity.id.id,
+ rootType: entity.id.entityType,
+ direction: types.entitySearchDirection.from,
+ relationTypeGroup: "COMMON",
+ maxLevel: 1
+ },
+ filters: [
+ {
+ relationType: "Contains",
+ entityTypes: []
+ }
+ ]
+ };
+ return query;
+ }
+
+ function prepareNodeIcon(nodeCtx) {
+ var iconInfo = vm.nodeIconFunction(nodeCtx);
+ if (iconInfo && iconInfo === 'default') {
+ iconInfo = defaultNodeIconFunction(nodeCtx);
+ }
+ if (iconInfo && (iconInfo.iconUrl || iconInfo.materialIcon)) {
+ if (iconInfo.materialIcon) {
+ return materialIconHtml(iconInfo.materialIcon);
+ } else {
+ return iconUrlHtml(iconInfo.iconUrl);
+ }
+ } else {
+ return "";
+ }
+ }
+
+ function materialIconHtml(materialIcon) {
+ return ''+materialIcon+' ';
+ }
+
+ function iconUrlHtml(iconUrl) {
+ return '
';
+ }
+
+ function defaultNodeIconFunction(nodeCtx) {
+ var materialIcon = 'insert_drive_file';
+ var entity = nodeCtx.entity;
+ if (entity && entity.id && entity.id.entityType) {
+ switch (entity.id.entityType) {
+ case 'function':
+ materialIcon = 'functions';
+ break;
+ case types.entityType.device:
+ materialIcon = 'devices_other';
+ break;
+ case types.entityType.asset:
+ materialIcon = 'domain';
+ break;
+ case types.entityType.tenant:
+ materialIcon = 'supervisor_account';
+ break;
+ case types.entityType.customer:
+ materialIcon = 'supervisor_account';
+ break;
+ case types.entityType.user:
+ materialIcon = 'account_circle';
+ break;
+ case types.entityType.dashboard:
+ materialIcon = 'dashboards';
+ break;
+ case types.entityType.alarm:
+ materialIcon = 'notifications_active';
+ break;
+ case types.entityType.entityView:
+ materialIcon = 'view_quilt';
+ break;
+ }
+ }
+ return {
+ materialIcon: materialIcon
+ };
+ }
+
+ function defaultNodeOpenedFunction(nodeCtx) {
+ return nodeCtx.level <= 4;
+ }
+
+ function defaultSortFunction(nodeCtx1, nodeCtx2) {
+ var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);
+ if (result === 0) {
+ result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);
+ }
+ return result;
+ }
+}
diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.scss b/ui/src/app/widget/lib/entities-hierarchy-widget.scss
new file mode 100644
index 0000000000..d0f02a1392
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-hierarchy-widget.scss
@@ -0,0 +1,110 @@
+/**
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-has-timewindow {
+ .tb-entities-hierarchy {
+ md-toolbar {
+ min-height: 60px;
+ max-height: 60px;
+ }
+ }
+}
+
+.tb-entities-hierarchy {
+ md-toolbar {
+ min-height: 39px;
+ max-height: 39px;
+ }
+
+ .tb-entities-nav-tree-panel {
+ overflow-x: auto;
+ overflow-y: auto;
+
+ .tb-nav-tree-container {
+ &.jstree-proton {
+ .jstree-anchor {
+ div.node-icon {
+ display: inline-block;
+ width: 22px;
+ height: 22px;
+ margin-right: 2px;
+ margin-bottom: 2px;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ background-position: center center;
+ background-size: 18px 18px;
+ }
+
+ md-icon.node-icon {
+ width: 22px;
+ min-width: 22px;
+ height: 22px;
+ min-height: 22px;
+ margin-right: 2px;
+ margin-bottom: 2px;
+ color: inherit;
+
+ &.material-icons { /* stylelint-disable-line selector-max-class */
+ font-size: 18px;
+ line-height: 22px;
+ text-align: center;
+ }
+ }
+
+ &.jstree-hovered:not(.jstree-clicked),
+ &.jstree-disabled {
+ div.node-icon { /* stylelint-disable-line selector-max-class */
+ opacity: .5;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@media (max-width: 768px) {
+ .tb-entities-hierarchy {
+ .tb-entities-nav-tree-panel {
+ .tb-nav-tree-container {
+ &.jstree-proton-responsive {
+ .jstree-anchor {
+ div.node-icon {
+ width: 40px;
+ height: 40px;
+ margin: 0;
+ background-size: 24px 24px;
+ }
+
+ md-icon.node-icon {
+ width: 40px;
+ min-width: 40px;
+ height: 40px;
+ min-height: 40px;
+ margin: 0;
+
+ &.material-icons { /* stylelint-disable-line selector-max-class */
+ font-size: 24px;
+ line-height: 40px;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html b/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html
new file mode 100644
index 0000000000..ab18748f0c
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ search
+
+ {{'entity.search' | translate}}
+
+
+
+
+
+
+
+ close
+
+ {{ 'action.close' | translate }}
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js
index ba1ae1f65e..a21f7a0876 100644
--- a/ui/src/app/widget/lib/google-map.js
+++ b/ui/src/app/widget/lib/google-map.js
@@ -338,15 +338,21 @@ export default class TbGoogleMap {
locationSettings: settings,
dsIndex: location.dsIndex
});
-
+ let map = this;
if (onClickListener) {
google.maps.event.addListener(polygon, 'click', function (event) {
- if (settings.displayTooltip) {
+ if (settings.displayTooltip ) {
+ if (settings.autocloseTooltip) {
+ map.tooltips.forEach((tooltip) => {
+ tooltip.popup.close();
+ });
+ }
if (!polygon.anchor) {
polygon.anchor = new google.maps.MVCObject();
}
polygon.anchor.set("position", event.latLng);
popup.open(this.map, polygon.anchor);
+
}
onClickListener();
});
@@ -367,8 +373,8 @@ export default class TbGoogleMap {
strokeColor: color,
fillColor: color,
strokeWeight: settings.polygonStrokeWeight
- }
-
+ };
+ polygon.setOptions(options);
}
/* eslint-disable no-undef ,no-unused-vars*/
diff --git a/ui/src/app/widget/lib/map-widget2.js b/ui/src/app/widget/lib/map-widget2.js
index 4762a3fda1..f330bd61bf 100644
--- a/ui/src/app/widget/lib/map-widget2.js
+++ b/ui/src/app/widget/lib/map-widget2.js
@@ -546,7 +546,13 @@ export default class TbMapWidgetV2 {
function mapPolygonArray (rawArray) {
let latLngArray = rawArray.map(function (el) {
if (el.length === 2) {
- return tbMap.map.createLatLng(el[0], el[1]);
+ if (!angular.isNumber(el[0]) && !angular.isNumber(el[1])) {
+ return el.map(function (subEl) {
+ return mapPolygonArray(subEl);
+ })
+ } else {
+ return tbMap.map.createLatLng(el[0], el[1]);
+ }
} else if (el.length > 2) {
return mapPolygonArray(el);
} else {
diff --git a/ui/src/app/widget/lib/tencent-map.js b/ui/src/app/widget/lib/tencent-map.js
index 6ce387e46a..9638667317 100644
--- a/ui/src/app/widget/lib/tencent-map.js
+++ b/ui/src/app/widget/lib/tencent-map.js
@@ -287,7 +287,7 @@ export default class TbTencentMap {
popup.open();
popup.setPosition(marker);
});
- this.tooltips.push({
+ map.tooltips.push({
markerArgs: markerArgs,
popup: popup,
locationSettings: settings,
@@ -353,6 +353,11 @@ export default class TbTencentMap {
if (onClickListener) {
qq.maps.event.addListener(polygon, 'click', function (event) {
+ if (settings.autocloseTooltip) {
+ map.tooltips.forEach((tooltip) => {
+ tooltip.popup.close();
+ });
+ }
if (settings.displayTooltip) {
popup.setMap(this.map);
popup.setPosition(event.latLng);
@@ -379,7 +384,7 @@ export default class TbTencentMap {
fillColor: color,
strokeWeight: settings.polygonStrokeWeight
}
-
+ polygon.setOptions(options);
}
/* eslint-disable no-undef ,no-unused-vars*/
diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js
new file mode 100644
index 0000000000..311efebf24
--- /dev/null
+++ b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js
@@ -0,0 +1,700 @@
+/*
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './trip-animation-widget.scss';
+import template from "./trip-animation-widget.tpl.html";
+import TbOpenStreetMap from '../openstreet-map';
+import L from 'leaflet';
+//import tinycolor from 'tinycolor2';
+import tinycolor from "tinycolor2";
+import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils";
+//import {fillPatternWithActions, isNumber, padValue, processPattern, fillPattern} from "../widget-utils";
+
+(function () {
+ // save these original methods before they are overwritten
+ var proto_initIcon = L.Marker.prototype._initIcon;
+ var proto_setPos = L.Marker.prototype._setPos;
+
+ var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
+
+ L.Marker.addInitHook(function () {
+ var iconOptions = this.options.icon && this.options.icon.options;
+ var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
+ if (iconAnchor) {
+ iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
+ }
+ this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom';
+ this.options.rotationAngle = this.options.rotationAngle || 0;
+
+ // Ensure marker keeps rotated during dragging
+ this.on('drag', function (e) {
+ e.target._applyRotation();
+ });
+ });
+
+ L.Marker.include({
+ _initIcon: function () {
+ proto_initIcon.call(this);
+ },
+
+ _setPos: function (pos) {
+ proto_setPos.call(this, pos);
+ this._applyRotation();
+ },
+
+ _applyRotation: function () {
+ if (this.options.rotationAngle) {
+ this._icon.style[L.DomUtil.TRANSFORM + 'Origin'] = this.options.rotationOrigin;
+
+ if (oldIE) {
+ // for IE 9, use the 2D rotation
+ this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
+ } else {
+ // for modern browsers, prefer the 3D accelerated version
+ let rotation = ' rotateZ(' + this.options.rotationAngle + 'deg)';
+ if (!this._icon.style[L.DomUtil.TRANSFORM].includes(rotation)) {
+ this._icon.style[L.DomUtil.TRANSFORM] += rotation;
+ }
+ }
+ }
+ },
+
+ setRotationAngle: function (angle) {
+ this.options.rotationAngle = angle;
+ this.update();
+ return this;
+ },
+
+ setRotationOrigin: function (origin) {
+ this.options.rotationOrigin = origin;
+ this.update();
+ return this;
+ }
+ });
+})();
+
+
+export default angular.module('thingsboard.widgets.tripAnimation', [])
+ .directive('tripAnimation', tripAnimationWidget)
+ .filter('tripAnimation', function ($filter) {
+ return function (label) {
+ label = label.toString();
+
+ let translateSelector = "widgets.tripAnimation." + label;
+ let translation = $filter('translate')(translateSelector);
+
+ if (translation !== translateSelector) {
+ return translation;
+ }
+
+ return label;
+ }
+ })
+ .name;
+
+
+/*@ngInject*/
+function tripAnimationWidget() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ ctx: '=',
+ self: '='
+ },
+ controller: tripAnimationController,
+ controllerAs: 'vm',
+ templateUrl: template
+ };
+}
+
+/*@ngInject*/
+function tripAnimationController($document, $scope, $http, $timeout, $filter) {
+ let vm = this;
+ //const varsRegex = /\$\{([^\}]*)\}/g;
+ //let icon;
+
+ vm.initBounds = true;
+
+ vm.markers = [];
+ vm.index = 0;
+ vm.dsIndex = 0;
+ vm.isPlaying = false;
+ vm.minTime = 0;
+ vm.maxTime = 0;
+ vm.isPLaying = false;
+ vm.trackingLine = {
+ "type": "FeatureCollection",
+ features: []
+ };
+ vm.speeds = [1, 5, 10, 25];
+ vm.speed = 1;
+ vm.trips = [];
+ vm.activeTripIndex = 0;
+
+ vm.showHideTooltip = showHideTooltip;
+ vm.recalculateTrips = recalculateTrips;
+
+ $scope.$watch('vm.ctx', function () {
+ if (vm.ctx) {
+ vm.utils = vm.ctx.$scope.$injector.get('utils');
+ vm.settings = vm.ctx.settings;
+ vm.widgetConfig = vm.ctx.widgetConfig;
+ vm.data = vm.ctx.data;
+ vm.datasources = vm.ctx.datasources;
+ configureStaticSettings();
+ initialize();
+ initializeCallbacks();
+ }
+ });
+
+
+ function initializeCallbacks() {
+ vm.self.onDataUpdated = function () {
+ createUpdatePath();
+ };
+
+ vm.self.onResize = function () {
+ resize();
+ };
+
+ vm.self.typeParameters = function () {
+ return {
+ maxDatasources: 1, // Maximum allowed datasources for this widget, -1 - unlimited
+ maxDataKeys: -1 //Maximum allowed data keys for this widget, -1 - unlimited
+ }
+ };
+ return true;
+ }
+
+
+ function resize() {
+ if (vm.map) {
+ vm.map.invalidateSize();
+ }
+ }
+
+ function initCallback() {
+ //createUpdatePath();
+ //resize();
+ }
+
+ vm.playMove = function (play) {
+ if (play && vm.isPLaying) return;
+ if (play || vm.isPLaying) vm.isPLaying = true;
+ if (vm.isPLaying) {
+ if (vm.index + 1 > vm.maxTime) return;
+ vm.index++;
+ vm.trips.forEach(function (trip) {
+ moveMarker(trip);
+ });
+ vm.timeout = $timeout(function () {
+ vm.playMove();
+ }, 1000 / vm.speed)
+ }
+ };
+
+
+ vm.stopPlay = function () {
+ vm.isPLaying = false;
+ $timeout.cancel(vm.timeout);
+ };
+
+ function recalculateTrips() {
+ vm.trips.forEach(function (value) {
+ moveMarker(value);
+ })
+ }
+
+ function findAngle(lat1, lng1, lat2, lng2) {
+ let angle = Math.atan2(0, 0) - Math.atan2(lat2 - lat1, lng2 - lng1);
+ angle = angle * 180 / Math.PI;
+ return parseInt(angle.toFixed(2));
+ }
+
+ function initialize() {
+ $scope.currentDate = $filter('date')(0, "yyyy.MM.dd HH:mm:ss");
+
+ vm.self.actionSources = [vm.searchAction];
+ vm.endpoint = vm.ctx.settings.endpointUrl;
+ $scope.title = vm.ctx.widgetConfig.title;
+ vm.utils = vm.self.ctx.$scope.$injector.get('utils');
+
+ vm.showTimestamp = vm.settings.showTimestamp !== false;
+ vm.ctx.$element = angular.element("#heat-map", vm.ctx.$container);
+ vm.defaultZoomLevel = 2;
+ if (vm.ctx.settings.defaultZoomLevel) {
+ if (vm.ctx.settings.defaultZoomLevel > 0 && vm.ctx.settings.defaultZoomLevel < 21) {
+ vm.defaultZoomLevel = Math.floor(vm.ctx.settings.defaultZoomLevel);
+ }
+ }
+ vm.dontFitMapBounds = vm.ctx.settings.fitMapBounds === false;
+ vm.map = new TbOpenStreetMap(vm.ctx.$element, vm.utils, initCallback, vm.defaultZoomLevel, vm.dontFitMapBounds, null, vm.staticSettings.mapProvider);
+ vm.map.bounds = vm.map.createBounds();
+ vm.map.invalidateSize(true);
+ vm.map.bounds = vm.map.createBounds();
+
+ vm.tooltipActionsMap = {};
+ var descriptors = vm.ctx.actionsApi.getActionDescriptors('tooltipAction');
+ descriptors.forEach(function (descriptor) {
+ if (descriptor) vm.tooltipActionsMap[descriptor.name] = descriptor;
+ });
+ }
+
+ function configureStaticSettings() {
+ let staticSettings = {};
+ vm.staticSettings = staticSettings;
+ //Calculate General Settings
+ staticSettings.mapProvider = vm.ctx.settings.mapProvider || "OpenStreetMap.Mapnik";
+ staticSettings.latKeyName = vm.ctx.settings.latKeyName || "latitude";
+ staticSettings.lngKeyName = vm.ctx.settings.lngKeyName || "longitude";
+ staticSettings.rotationAngle = vm.ctx.settings.rotationAngle || 0;
+ staticSettings.displayTooltip = vm.ctx.settings.showTooltip || false;
+ staticSettings.defaultZoomLevel = vm.ctx.settings.defaultZoomLevel || true;
+ staticSettings.showTooltip = false;
+ staticSettings.label = vm.ctx.settings.label || "${entityName}";
+ staticSettings.useLabelFunction = vm.ctx.settings.useLabelFunction || false;
+ staticSettings.showLabel = vm.ctx.settings.showLabel || false;
+ staticSettings.useTooltipFunction = vm.ctx.settings.useTooltipFunction || false;
+ staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "${entityName} Latitude: ${latitude:7}Longitude: ${longitude:7}Start Time: ${maxTime}End Time: ${minTime}";
+ staticSettings.tooltipOpacity = vm.ctx.settings.tooltipOpacity || 1;
+ staticSettings.tooltipColor = vm.ctx.settings.tooltipColor ? tinycolor(vm.ctx.settings.tooltipColor).toHexString() : "#ffffff";
+ staticSettings.tooltipFontColor = vm.ctx.settings.tooltipFontColor ? tinycolor(vm.ctx.settings.tooltipFontColor).toHexString() : "#000000";
+ staticSettings.pathColor = vm.ctx.settings.color ? tinycolor(vm.ctx.settings.color).toHexString() : "#ff6300";
+ staticSettings.pathWeight = vm.ctx.settings.strokeWeight || 1;
+ staticSettings.pathOpacity = vm.ctx.settings.strokeOpacity || 1;
+ staticSettings.usePathColorFunction = vm.ctx.settings.useColorFunction || false;
+ staticSettings.showPoints = vm.ctx.settings.showPoints || false;
+ staticSettings.pointSize = vm.ctx.settings.pointSize || 1;
+ staticSettings.markerImageSize = vm.ctx.settings.markerImageSize || 20;
+ staticSettings.useMarkerImageFunction = vm.ctx.settings.useMarkerImageFunction || false;
+ staticSettings.pointColor = vm.ctx.settings.pointColor ? tinycolor(vm.ctx.settings.pointColor).toHexString() : "#ff6300";
+ staticSettings.markerImages = vm.ctx.settings.markerImages || [];
+ staticSettings.icon = L.icon({
+ iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAACqCAYAAAA9dtSCAAAAhnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjadY7LDcAwCEPvTNERCBA+40RVI3WDjl+iNMqp7wCWBZbheu4Ox6AggVRzDVVMJCSopXCcMGIhLGPnnHybSyraNjBNoeGGsg/l8xeV1bWbmGnVU0/KdLqY2HPmH4xUHDVih7S2Gv34q8ULVzos2Vmq5r4AAAoGaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICBleGlmOlBpeGVsWERpbWVuc2lvbj0iMTcwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTcwIgogICB0aWZmOkltYWdlV2lkdGg9IjE3MCIKICAgdGlmZjpJbWFnZUhlaWdodD0iMTcwIgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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",
+ iconSize: [30, 30],
+ iconAnchor: [15, 15]
+ });
+ if (angular.isDefined(vm.ctx.settings.markerImage)) {
+ staticSettings.icon = L.icon({
+ iconUrl: vm.ctx.settings.markerImage,
+ iconSize: [staticSettings.markerImageSize, staticSettings.markerImageSize],
+ iconAnchor: [(staticSettings.markerImageSize / 2), (staticSettings.markerImageSize / 2)]
+ })
+ }
+
+ if (staticSettings.usePathColorFunction && angular.isDefined(vm.ctx.settings.colorFunction)) {
+ staticSettings.colorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.colorFunction);
+ }
+
+ if (staticSettings.useLabelFunction && angular.isDefined(vm.ctx.settings.labelFunction)) {
+ staticSettings.labelFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.labelFunction);
+ }
+
+ if (staticSettings.useTooltipFunction && angular.isDefined(vm.ctx.settings.tooltipFunction)) {
+ staticSettings.tooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.tooltipFunction);
+ }
+
+ if (staticSettings.useMarkerImageFunction && angular.isDefined(vm.ctx.settings.markerImageFunction)) {
+ staticSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', vm.ctx.settings.markerImageFunction);
+ }
+
+ if (!staticSettings.useMarkerImageFunction &&
+ angular.isDefined(vm.ctx.settings.markerImage) &&
+ vm.ctx.settings.markerImage.length > 0) {
+ staticSettings.useMarkerImage = true;
+ let url = vm.ctx.settings.markerImage;
+ let size = staticSettings.markerImageSize || 20;
+ staticSettings.currentImage = {
+ url: url,
+ size: size
+ };
+ vm.utils.loadImageAspect(staticSettings.currentImage.url).then(
+ (aspect) => {
+ if (aspect) {
+ let width;
+ let height;
+ if (aspect > 1) {
+ width = staticSettings.currentImage.size;
+ height = staticSettings.currentImage.size / aspect;
+ } else {
+ width = staticSettings.currentImage.size * aspect;
+ height = staticSettings.currentImage.size;
+ }
+ staticSettings.icon = L.icon({
+ iconUrl: staticSettings.currentImage.url,
+ iconSize: [width, height],
+ iconAnchor: [width / 2, height / 2]
+ });
+ }
+ if (vm.trips) {
+ vm.trips.forEach(function (trip) {
+ if (trip.marker) {
+ trip.marker.setIcon(staticSettings.icon);
+ }
+ });
+ }
+ }
+ )
+ }
+ }
+
+ function configureTripSettings(trip) {
+ trip.settings = {};
+ trip.settings.color = calculateColor(trip);
+ trip.settings.strokeWeight = vm.staticSettings.pathWeight;
+ trip.settings.strokeOpacity = vm.staticSettings.pathOpacity;
+ trip.settings.pointColor = vm.staticSettings.pointColor;
+ trip.settings.pointSize = vm.staticSettings.pointSize;
+ trip.settings.labelText = calculateLabel(trip);
+ trip.settings.tooltipText = calculateTooltip(trip);
+ trip.settings.icon = calculateIcon(trip);
+ }
+
+ function calculateLabel(trip) {
+ let label = '';
+ if (vm.staticSettings.showLabel) {
+ let labelReplaceInfo;
+ let labelText = vm.staticSettings.label;
+ if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
+ try {
+ labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dsIndex);
+ } catch (e) {
+ labelText = null;
+ }
+ }
+ labelText = vm.utils.createLabelFromDatasource(trip.dataSource, labelText);
+ labelReplaceInfo = processPattern(labelText, vm.ctx.datasources, trip.dSIndex);
+ label = fillPattern(labelText, labelReplaceInfo, trip.timeRange[vm.index]);
+ if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
+ try {
+ labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
+ } catch (e) {
+ labelText = null;
+ }
+ }
+ }
+ return label;
+ }
+
+ function calculateTooltip(trip) {
+ let tooltip = '';
+ if (vm.staticSettings.displayTooltip) {
+ let tooltipReplaceInfo;
+ let tooltipText = vm.staticSettings.tooltipPattern;
+ if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) {
+ try {
+ tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
+ } catch (e) {
+ tooltipText = null;
+ }
+ }
+ tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
+ tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
+ tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[vm.index]);
+ tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);
+
+ }
+ return tooltip;
+ }
+
+ function calculateColor(trip) {
+ let color = vm.staticSettings.pathColor;
+ let colorFn;
+ if (vm.staticSettings.usePathColorFunction && angular.isDefined(vm.staticSettings.colorFunction)) {
+ try {
+ colorFn = vm.staticSettings.colorFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
+ } catch (e) {
+ colorFn = null;
+ }
+ }
+ if (colorFn && colorFn !== color && trip.polyline) {
+ trip.polyline.setStyle({color: colorFn});
+ }
+ return colorFn || color;
+ }
+
+ function calculateIcon(trip) {
+ let icon = vm.staticSettings.icon;
+ if (vm.staticSettings.useMarkerImageFunction && angular.isDefined(vm.staticSettings.markerImageFunction)) {
+ let rawIcon;
+ try {
+ rawIcon = vm.staticSettings.markerImageFunction(vm.ctx.data, vm.staticSettings.markerImages, trip.timeRange[vm.index], trip.dSIndex);
+ } catch (e) {
+ rawIcon = null;
+ }
+ if (rawIcon) {
+ vm.utils.loadImageAspect(rawIcon).then(
+ (aspect) => {
+ if (aspect) {
+ let width;
+ let height;
+ if (aspect > 1) {
+ width = rawIcon.size;
+ height = rawIcon.size / aspect;
+ } else {
+ width = rawIcon.size * aspect;
+ height = rawIcon.size;
+ }
+ icon = L.icon({
+ iconUrl: rawIcon,
+ iconSize: [width, height],
+ iconAnchor: [width / 2, height / 2]
+ });
+ }
+ if (trip.marker) {
+ trip.marker.setIcon(icon);
+ }
+ }
+ )
+ }
+ }
+ return icon;
+ }
+
+ function createUpdatePath() {
+ if (vm.trips && vm.map) {
+ vm.trips.forEach(function (trip) {
+ if (trip.marker) {
+ trip.marker.remove();
+ delete trip.marker;
+ }
+ if (trip.polyline) {
+ trip.polyline.remove();
+ delete trip.polyline;
+ }
+ if (trip.points && trip.points.length) {
+ trip.points.forEach(function (point) {
+ point.remove();
+ });
+ delete trip.points;
+ }
+ });
+ vm.initBounds = true;
+ }
+ let normalizedTimeRange = createNormalizedTime(vm.data, 1000);
+ createNormalizedTrips(normalizedTimeRange, vm.datasources);
+ createTripsOnMap();
+ if (vm.initBounds && !vm.initTrips) {
+ vm.trips.forEach(function (trip) {
+ vm.map.extendBounds(vm.map.bounds, trip.polyline);
+ vm.initBounds = !vm.datasources.every(
+ function (ds) {
+ return ds.dataReceived === true;
+ });
+ vm.initTrips = vm.trips.every(function (trip) {
+ return angular.isDefined(trip.marker) && angular.isDefined(trip.polyline);
+ });
+ });
+
+ vm.map.fitBounds(vm.map.bounds);
+ }
+
+ }
+
+ function fillPattern(pattern, replaceInfo, currentNormalizedValue) {
+ let text = angular.copy(pattern);
+ let reg = /\$\{([^\}]*)\}/g;
+ if (replaceInfo) {
+ for (let v = 0; v < replaceInfo.variables.length; v++) {
+ let variableInfo = replaceInfo.variables[v];
+ let label = reg.exec(pattern)[1].split(":")[0];
+ let txtVal = '';
+ if (label.length > -1 && angular.isDefined(currentNormalizedValue[label])) {
+ let varData = currentNormalizedValue[label];
+ if (isNumber(varData)) {
+ txtVal = padValue(varData, variableInfo.valDec, 0);
+ } else {
+ txtVal = varData;
+ }
+ }
+ text = text.split(variableInfo.variable).join(txtVal);
+ }
+ }
+ return text;
+ }
+
+ function createNormalizedTime(data, step) {
+ if (!step) step = 1000;
+ let max_time = null;
+ let min_time = null;
+ let normalizedArray = [];
+ if (data && data.length > 0) {
+ vm.data.forEach(function (data) {
+ if (data.data.length > 0) {
+ data.data.forEach(function (sData) {
+ if (max_time === null) {
+ max_time = sData[0];
+ } else if (max_time < sData[0]) {
+ max_time = sData[0]
+ }
+ if (min_time === null) {
+ min_time = sData[0];
+ } else if (min_time > sData[0]) {
+ min_time = sData[0];
+ }
+ })
+ }
+ });
+ for (let i = min_time; i < max_time; i += step) {
+ normalizedArray.push({ts: i})
+ }
+ if (normalizedArray[normalizedArray.length - 1] && normalizedArray[normalizedArray.length - 1].ts !== max_time) normalizedArray.push({ts: max_time});
+ }
+ vm.maxTime = normalizedArray.length - 1;
+ vm.minTime = 0;
+ return normalizedArray;
+ }
+
+ function createNormalizedTrips(timeRange, dataSources) {
+ vm.trips = [];
+ if (timeRange && timeRange.length > 0 && dataSources && dataSources.length > 0 && vm.data && vm.data.length > 0) {
+ dataSources.forEach(function (dS, index) {
+ vm.trips.push({
+ dataSource: dS,
+ dSIndex: index,
+ timeRange: angular.copy(timeRange)
+ })
+ });
+
+ vm.data.forEach(function (data) {
+ let ds = data.datasource;
+ let tripIndex = vm.trips.findIndex(function (el) {
+ return el.dataSource.entityId === ds.entityId;
+ });
+
+ if (tripIndex > -1) {
+ createNormalizedValue(data.data, data.dataKey.label, vm.trips[tripIndex].timeRange);
+ }
+ })
+ }
+
+ createNormalizedLatLngs();
+ }
+
+ function createNormalizedValue(dataArray, dataKey, timeRangeArray) {
+ timeRangeArray.forEach(function (timeStamp) {
+ let targetTDiff = null;
+ let value = null;
+ for (let i = 0; i < dataArray.length; i++) {
+ let tDiff = dataArray[i][0] - timeStamp.ts;
+ if (targetTDiff === null || (tDiff <= 0 && targetTDiff < tDiff)) {
+ targetTDiff = tDiff;
+ value = dataArray[i][1];
+
+ }
+ }
+ if (value !== null) timeStamp[dataKey] = value;
+ });
+ }
+
+ function createNormalizedLatLngs() {
+ vm.trips.forEach(function (el) {
+ el.latLngs = [];
+ el.timeRange.forEach(function (data) {
+ let lat = data[vm.staticSettings.latKeyName];
+ let lng = data[vm.staticSettings.lngKeyName];
+ if (lat && lng && vm.map) {
+ data.latLng = vm.map.createLatLng(lat, lng);
+ }
+ el.latLngs.push(data.latLng);
+ });
+ addAngleForTip(el);
+ })
+ }
+
+ function addAngleForTip(trip) {
+ if (trip.timeRange && trip.timeRange.length > 0) {
+ trip.timeRange.forEach(function (point, index) {
+ let nextPoint, prevPoint;
+ nextPoint = index === (trip.timeRange.length - 1) ? trip.timeRange[index] : trip.timeRange[index + 1];
+ prevPoint = index === 0 ? trip.timeRange[0] : trip.timeRange[index - 1];
+ point.h = findAngle(prevPoint[vm.staticSettings.latKeyName], prevPoint[vm.staticSettings.lngKeyName], nextPoint[vm.staticSettings.latKeyName], nextPoint[vm.staticSettings.lngKeyName]);
+ point.h += vm.staticSettings.rotationAngle;
+ });
+ }
+ }
+
+ function createTripsOnMap() {
+ if (vm.trips.length > 0) {
+ vm.trips.forEach(function (trip) {
+ if (trip.timeRange.length > 0 && trip.latLngs.every(el => angular.isDefined(el))) {
+ configureTripSettings(trip, vm.index);
+ if (vm.staticSettings.showPoints) {
+ trip.points = [];
+ trip.latLngs.forEach(function (latLng) {
+ let point = L.circleMarker(latLng, {
+ color: trip.settings.pointColor,
+ radius: trip.settings.pointSize
+ }).addTo(vm.map.map);
+ trip.points.push(point);
+ });
+ }
+
+ if (angular.isUndefined(trip.marker)) {
+ trip.polyline = vm.map.createPolyline(trip.latLngs, trip.settings);
+ }
+
+ if (trip.timeRange && trip.timeRange.length && angular.isUndefined(trip.marker)) {
+ trip.marker = L.marker(trip.timeRange[vm.index].latLng).addTo(vm.map.map);
+ trip.marker.setZIndexOffset(1000);
+ trip.marker.setIcon(vm.staticSettings.icon);
+ trip.marker.setRotationOrigin('center center');
+ // trip.marker.addTo(vm.map.map);
+ trip.marker.on('click', function () {
+ showHideTooltip(trip);
+ });
+ moveMarker(trip);
+ }
+ }
+ });
+ }
+ }
+
+ function moveMarker(trip) {
+ if (angular.isDefined(trip.marker)) {
+ trip.markerAngleIsSet = true;
+ trip.marker.setLatLng(trip.timeRange[vm.index].latLng);
+ trip.marker.setRotationAngle(trip.timeRange[vm.index].h);
+ trip.marker.update();
+ } else {
+ if (trip.timeRange && trip.timeRange.length) {
+ trip.marker = L.marker(trip.timeRange[vm.index].latLng);
+ trip.marker.setZIndexOffset(1000);
+ trip.marker.setIcon(vm.staticSettings.icon);
+ trip.marker.setRotationOrigin('center center');
+ trip.marker.addTo(vm.map.map);
+ trip.marker.on('click', function () {
+ showHideTooltip(trip);
+ });
+ trip.marker.update();
+ }
+
+ }
+ configureTripSettings(trip);
+ }
+
+
+ function showHideTooltip(trip) {
+ if (vm.staticSettings.displayTooltip) {
+ if (vm.staticSettings.showTooltip && trip && vm.activeTripIndex !== trip.dSIndex) {
+ vm.staticSettings.showTooltip = true;
+ } else {
+ vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
+ }
+ }
+ if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss
new file mode 100644
index 0000000000..1b6ae956c3
--- /dev/null
+++ b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.heat-map-widget {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ font-size: 16px;
+ line-height: 24px;
+}
+
+.heat-map-info-panel {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 2;
+ background-color: rgba(0, 0, 0, .3);
+ border-bottom-left-radius: 5px;
+
+ .md-button {
+ min-width: auto;
+ }
+}
+
+.heat-map-tooltip {
+ position: absolute;
+ top: 47px;
+ right: 0;
+ z-index: 2;
+ padding: 10px;
+ background-color: #fff;
+ border-top-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ transition: .3s ease-in-out;
+
+ &-hidden {
+ transform: translateX(100%);
+ }
+}
+
+.heat-map-title {
+ padding: 10px;
+}
+
+.heat-map-control-panel {
+ position: absolute;
+ bottom: 0;
+ z-index: 2;
+ box-sizing: border-box;
+ width: 100%;
+ padding-right: 70px;
+ padding-left: 20px;
+ background: rgba(0, 0, 0, .3);
+
+ md-slider-container {
+ button {
+ max-width: none;
+
+ ng-md-icon {
+ width: 36px;
+ height: 36px;
+
+ svg {
+ width: inherit;
+ height: inherit;
+ }
+ }
+ }
+
+ .panel-timer {
+ max-width: none;
+ font-size: 20px;
+ font-weight: 600;
+ }
+ }
+}
+
+.heat-map-container {
+ position: relative;
+ z-index: 1;
+ flex: 1;
+ width: 100%;
+}
+
+#heat-map {
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+
+ .pointsLayerMarkerIcon {
+ border-radius: 50%;
+ }
+}
diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html
new file mode 100644
index 0000000000..5ffc6d56f9
--- /dev/null
+++ b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html
@@ -0,0 +1,54 @@
+
+
\ No newline at end of file
diff --git a/ui/src/png/jstree/32px.png b/ui/src/png/jstree/32px.png
new file mode 100644
index 0000000000..719a6bcdbd
Binary files /dev/null and b/ui/src/png/jstree/32px.png differ
diff --git a/ui/src/png/jstree/40px.png b/ui/src/png/jstree/40px.png
new file mode 100644
index 0000000000..1959347aea
Binary files /dev/null and b/ui/src/png/jstree/40px.png differ