diff --git a/application/pom.xml b/application/pom.xml index 962ff10d4c..c348cc7ba8 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.0 + 2.4.1 thingsboard application @@ -52,6 +52,10 @@ linux-x86_64 + + org.thingsboard.common + util + org.thingsboard.rule-engine rule-engine-api @@ -200,10 +204,6 @@ io.grpc grpc-stub - - io.springfox - springfox-swagger-ui - io.springfox springfox-swagger2 @@ -272,6 +272,14 @@ io.springfox.ui springfox-swagger-ui-rfc6570 + + org.passay + passay + + + com.github.ua-parser + uap-java + 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 60124c0fc3..dab5e7976e 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -31,8 +31,8 @@ "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.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}" } @@ -112,7 +112,7 @@ "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.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\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\":{}}" } diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 71e9fa7ace..2ecd90be28 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -71,10 +71,10 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema;\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema;\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", "settingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, { @@ -170,4 +170,4 @@ } } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json new file mode 100644 index 0000000000..d9490e0c3b --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json @@ -0,0 +1,41 @@ +{ + "widgetsBundle": { + "alias": "entity_admin_widgets", + "title": "Entity admin widgets", + "image": null + }, + "widgetTypes": [ + { + "alias": "device_admin_table2", + "name": "Device admin table", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 6.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.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\"},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n \\n Add device\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n \\n \\n \\n \\n Label\\n \\n \\n \\n \\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n \\n \\n \\n \\n \\n Create\\n Cancel\\n \\n \\n\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddDeviceDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addDeviceForm.$setPristine();\\n let device = {\\n name: vm.deviceName,\\n type: vm.deviceType,\\n label: vm.deviceLabel\\n };\\n deviceService.saveDevice(device).then(\\n (device) => {\\n saveAttributes(device.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n \\n Edit device\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n \\n \\n \\n \\n Label\\n \\n \\n \\n \\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n \\n \\n \\n \\n \\n Create\\n Cancel\\n \\n \\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditDeviceDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n deviceService.getDevice(entityId.id).then(\\n (device) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.device = device;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editDeviceForm.$setPristine();\\n deviceService.saveDevice(vm.device).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteDevice();\\n })\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}" + } + }, + { + "alias": "device_admin_table", + "name": "Asset admin table", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 6.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.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\"},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n \\n Add asset\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n \\n \\n \\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n \\n \\n \\n \\n \\n Create\\n Cancel\\n \\n \\n\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddAssetDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addAssetForm.$setPristine();\\n let asset = {\\n name: vm.assetName,\\n type: vm.assetType\\n };\\n assetService.saveAsset(asset).then(\\n (asset) => {\\n saveAttributes(asset.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n \\n Edit asset\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n \\n \\n \\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n \\n \\n \\n \\n \\n Create\\n Cancel\\n \\n \\n\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditAssetDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n assetService.getAsset(entityId.id).then(\\n (asset) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.asset = asset;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editAssetForm.$setPristine();\\n assetService.saveAsset(vm.asset).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteAsset();\\n })\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}" + } + } + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 6dc7f2aad0..5129ea19a7 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -13,9 +13,9 @@ "sizeX": 7.5, "sizeY": 3.5, "resources": [], - "templateHtml": "\n \n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update server attribute\n \n \n close\n Discard changes\n \n \n \n \n \n No entity selected\n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n \n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\r\n $scope.labelValue = settings.labelValue || \"Value\";\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -29,9 +29,9 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update server attribute\n \n \n close\n Discard changes\n \n \n \n\n \n No entity selected\n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n\n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n $scope.labelValue = settings.labelValue || \"Value\";\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -45,9 +45,9 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update server attribute\n \n \n close\n Discard changes\n \n \n \n\n \n No entity selected\n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n\n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n $scope.labelValue = settings.labelValue || \"Value\";\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -61,14 +61,46 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n \n \n \n {{currentValue}}\n \n \n \n\n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n \n \n \n {{currentValue}}\n \n \n \n\n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = 'No entity selected';\n\n settings.trueValue = settings.trueValue || true;\n settings.falseValue = settings.falseValue || false;\n\n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:true, [settings.falseValue]:false};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: mapReverse[$scope.currentValue] || false\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = ($scope.originalValue = self.ctx.data[0].data[0][1]) || false;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onResize = function() {}\nself.onDestroy = function() {}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.customTranslation(settings.trueValue, settings.trueValue) || true;\n settings.falseValue = utils.customTranslation(settings.falseValue, settings.falseValue) || false;\n\n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:true, [settings.falseValue]:false};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: mapReverse[$scope.currentValue] || false\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = ($scope.originalValue = self.ctx.data[0].data[0][1]) || false;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onResize = function() {}\nself.onDestroy = function() {}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, + { + "alias": "update_server_date_attribute", + "name": "Update server date attribute", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n\t \n\t \n {{requiredErrorMessage}}\n \n\t \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n \n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .action-button {\n margin: 0;\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mdp-date-picker,\n.attribute-update-form mdp-time-picker {\n width: 100%;\n}\n\n.attribute-update-form mdp-date-picker md-input-container,\n.attribute-update-form mdp-time-picker md-input-container {\n margin: 18px 0 5px;\n width: 100%;\n}\n\n.attribute-update-form.small-width mdp-date-picker md-input-container,\n.attribute-update-form.small-width mdp-time-picker md-input-container {\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.isValidParameter = true;\r\n $scope.entityDetected = false;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds = $scope.currentValue.getTime();\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n $scope.currentValue = $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n $scope.$digest();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show time input field\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"showTimeInput\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"requiredErrorMessage\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, + { + "alias": "update_server_image_attribute", + "name": "Update server image attribute", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n \n \n \n \n \n \n dashboard.no-image\n \n \n \n \n {{ 'action.remove' | translate }}\n close\n \n \n \n dashboard.drop-image\n \n \n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n \n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-image-select-wrapper {\n width: 100%;\n}\n\n.tb-image-select-wrapper>label {\n display: none;\n}\n\n.tb-image-select-wrapper>label.show-label {\n display: block;\n}\n\n.tb-image-preview-container,\n.tb-flow-drop {\n box-sizing: border-box;\n}\n\n.tb-image-preview {\n max-width: 98px;\n max-height: 98px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = self.ctx.settings || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.server.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n \n $scope.imageAdded = function ($file) {\n var reader = new FileReader();\n reader.onload = function(event) {\n $scope.$apply(function() {\n if (event.target.result && event.target.result.startsWith('data:image/')) {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = event.target.result;\n } \n });\n };\n reader.readAsDataURL($file.file);\n };\n \n $scope.clearImage = function () {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = null;\n };\n \n};\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateImageAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayPreview\":{\n \"title\":\"Display preview\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayClearButton\":{\n \"title\":\"Display clear button\",\n \"type\":\"boolean\",\n \"default\":false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"displayPreview\",\n \"displayClearButton\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, { "alias": "update_shared_string_attribute", "name": "Update shared string attribute", @@ -77,9 +109,9 @@ "sizeX": 7.5, "sizeY": 3.5, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update shared attribute\n \n \n close\n Discard changes\n \n \n \n\n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = 'No entity selected';\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n $scope.labelValue = settings.labelValue || \"Value\";\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === \"DEVICE\") {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = 'Selected entity cannot have shared attributes';\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -93,9 +125,9 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update shared attribute\n \n \n close\n Discard changes\n \n \n \n\n \n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n\n \n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n $scope.labelValue = settings.labelValue || \"Value\";\n $scope.message = 'No entity selected';\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === \"DEVICE\") {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = 'Selected entity cannot have share attribute';\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -109,9 +141,9 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update shared attribute\n \n \n close\n Discard changes\n \n \n \n\n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n $scope.labelValue = settings.labelValue || \"Value\";\n $scope.message = 'No entity selected';\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === \"DEVICE\") {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = 'Selected entity cannot have shared attributes';\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -125,14 +157,46 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n \n \n \n {{currentValue}}\n \n \n \n\n \n \n No attribute is selected\n \n \n Timeseries parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n \n \n \n {{currentValue}}\n \n \n \n\n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = 'No entity selected';\n\n settings.trueValue = settings.trueValue || true;\n settings.falseValue = settings.falseValue || false;\n\n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:true, [settings.falseValue]:false};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === \"DEVICE\") {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = 'Selected entity cannot have shared attributes';\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: mapReverse[$scope.currentValue] || false\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n $scope.checkboxValue = ($scope.originalValue = self.ctx.data[0].data[0][1]) || 'false';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n\n } catch (e) {\n console.log(e);\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onResize = function() {}\nself.onDestroy = function() {}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.customTranslation(settings.trueValue, settings.trueValue) || true;\n settings.falseValue = utils.customTranslation(settings.falseValue, settings.falseValue) || false;\n\n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:true, [settings.falseValue]:false};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: mapReverse[$scope.currentValue] || false\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n $scope.checkboxValue = ($scope.originalValue = self.ctx.data[0].data[0][1]) || 'false';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n\n } catch (e) {\n console.log(e);\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onResize = function() {}\nself.onDestroy = function() {}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"trueValue\":\"active\",\"falseValue\":\"inactive\"},\"title\":\"Update shared boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, + { + "alias": "update_shared_date_attribute", + "name": "Update shared date attribute", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n\t \n\t \n {{requiredErrorMessage}}\n \n\t \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n \n \n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .action-button {\n margin: 0;\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mdp-date-picker,\n.attribute-update-form mdp-time-picker {\n width: 100%;\n}\n\n.attribute-update-form mdp-date-picker md-input-container,\n.attribute-update-form mdp-time-picker md-input-container {\n margin: 18px 0 5px;\n width: 100%;\n}\n\n.attribute-update-form.small-width mdp-date-picker md-input-container,\n.attribute-update-form.small-width mdp-time-picker md-input-container {\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.isValidParameter = true;\r\n $scope.entityDetected = false;\r\n $scope.dataKeyDetected = false;\r\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-attribute-required');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType === types.entityType.device) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds = $scope.currentValue.getTime();\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.shared.value,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n $scope.currentValue = $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n $scope.$digest();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show time input field\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"showTimeInput\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"requiredErrorMessage\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, + { + "alias": "update_shared_image_attribute", + "name": "Update shared image attribute", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n \n \n \n \n \n \n dashboard.no-image\n \n \n \n \n {{ 'action.remove' | translate }}\n close\n \n \n \n dashboard.drop-image\n \n \n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-attribute' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n \n \n \n \n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n \n \n", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-image-select-wrapper {\n width: 100%;\n}\n\n.tb-image-select-wrapper>label {\n display: none;\n}\n\n.tb-image-select-wrapper>label.show-label {\n display: block;\n}\n\n.tb-image-preview-container,\n.tb-flow-drop {\n box-sizing: border-box;\n}\n\n.tb-image-preview {\n max-width: 98px;\n max-height: 98px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n settings = self.ctx.settings || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType === types.entityType.device) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n } else {\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.attribute) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entityType,\n datasource.entityId,\n types.attributesScope.shared.value,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n \n $scope.imageAdded = function ($file) {\n var reader = new FileReader();\n reader.onload = function(event) {\n $scope.$apply(function() {\n if (event.target.result && event.target.result.startsWith('data:image/')) {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = event.target.result;\n } \n });\n };\n reader.readAsDataURL($file.file);\n };\n \n $scope.clearImage = function () {\n $scope.attrUpdateForm.$setDirty();\n $scope.currentValue = null;\n };\n \n};\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateImageAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayPreview\":{\n \"title\":\"Display preview\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayClearButton\":{\n \"title\":\"Display clear button\",\n \"type\":\"boolean\",\n \"default\":false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"displayPreview\",\n \"displayClearButton\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, { "alias": "update_string_timeseries", "name": "Update string timeseries", @@ -141,9 +205,9 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update string timeseries\n \n \n close\n Discard changes\n \n \n \n \n \n No entity selected\n \n \n No timeseries is selected\n \n \n Attribute parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n \n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $q\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $q = $scope.$injector.get('$q');\r\n $http = $scope.$injector.get('$http');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\r\n $scope.labelValue = settings.labelValue || \"Value\";\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != \"timeseries\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n var deferred = $q.defer();\r\n var telemetriesData = {};\r\n for (var a = 0; a < telemetries.length; a++) {\r\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n }\r\n }\r\n if (Object.keys(telemetriesData).length) {\r\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n $http.post(url, telemetriesData).then(\r\n function(response) {\r\n deferred.resolve(response.data);\r\n },\r\n function() {\r\n deferred.reject();\r\n }\r\n );\r\n }\r\n return deferred.promise;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\nlet $q\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n $q = $scope.$injector.get('$q');\r\n $http = $scope.$injector.get('$http');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-timeseries-required');\r\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n var deferred = $q.defer();\r\n var telemetriesData = {};\r\n for (var a = 0; a < telemetries.length; a++) {\r\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n }\r\n }\r\n if (Object.keys(telemetriesData).length) {\r\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n $http.post(url, telemetriesData).then(\r\n function(response) {\r\n deferred.resolve(response.data);\r\n },\r\n function() {\r\n deferred.reject();\r\n }\r\n );\r\n }\r\n return deferred.promise;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update string timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -157,9 +221,9 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n \n \n \n {{currentValue}}\n \n \n \n\n \n \n No timeseries is selected\n \n \n Attribute parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n \n \n \n {{currentValue}}\n \n \n \n\n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.entity-title {\r\n font-weight: bold;\r\n font-size: 22px;\r\n padding-top: 12px;\r\n padding-bottom: 6px;\r\n color: #666;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .md-button.md-icon-button {\r\n margin: 0;\r\n}\r\n\r\n.attribute-update-form .md-button.md-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .md-icon-button md-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n\r\nmd-toast{\r\n min-width: 0;\r\n}\r\nmd-toast .md-toast-content {\r\n font-size: 14px!important;\r\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $q\nlet $http;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $q = $scope.$injector.get('$q');\n $http = $scope.$injector.get('$http');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = 'No entity selected';\n\n settings.trueValue = settings.trueValue || true;\n settings.falseValue = settings.falseValue || false;\n \n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var deferred = $q.defer();\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n $http.post(url, telemetriesData).then(\n function(response) {\n deferred.resolve(response.data);\n },\n function() {\n deferred.reject();\n }\n );\n }\n return deferred.promise;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet $q\nlet $http;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n $q = $scope.$injector.get('$q');\n $http = $scope.$injector.get('$http');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.customTranslation(settings.trueValue, settings.trueValue) || true;\n settings.falseValue = utils.customTranslation(settings.falseValue, settings.falseValue) || false;\n \n map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n $scope.checkboxValue = \"false\";\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.changed = function () {\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n }\n \n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var deferred = $q.defer();\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n $http.post(url, telemetriesData).then(\n function(response) {\n deferred.resolve(response.data);\n },\n function() {\n deferred.reject();\n }\n );\n }\n return deferred.promise;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.$digest();\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"trueValue\":\"active\",\"falseValue\":\"inactive\"},\"title\":\"Update boolean timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -173,9 +237,9 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update server attribute\n \n \n close\n Discard changes\n \n \n \n\n \n No entity selected\n \n \n No timeseries is selected\n \n \n Attribute parameter can not be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n\n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $q;\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $q = $scope.$injector.get('$q');\r\n $http = $scope.$injector.get('$http');\r\n settings = angular.copy(self.ctx.settings) || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\r\n $scope.labelValue = settings.labelValue || \"Value\";\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != \"timeseries\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n var deferred = $q.defer();\r\n var telemetriesData = {};\r\n for (var a = 0; a < telemetries.length; a++) {\r\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n }\r\n }\r\n if (Object.keys(telemetriesData).length) {\r\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n $http.post(url, telemetriesData).then(\r\n function(response) {\r\n deferred.resolve(response.data);\r\n },\r\n function() {\r\n deferred.reject();\r\n }\r\n );\r\n }\r\n return deferred.promise;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n correctValue($scope.currentValue);\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n $scope.currentValue = 0;\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\nlet $q;\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n $q = $scope.$injector.get('$q');\r\n $http = $scope.$injector.get('$http');\r\n settings = angular.copy(self.ctx.settings) || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-timeseries-required');\r\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.currentValue\r\n }\r\n ]\r\n ).then(\r\n function success() {\r\n $scope.originalValue = $scope.currentValue;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.currentValue === $scope.originalValue) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n var deferred = $q.defer();\r\n var telemetriesData = {};\r\n for (var a = 0; a < telemetries.length; a++) {\r\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n }\r\n }\r\n if (Object.keys(telemetriesData).length) {\r\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n $http.post(url, telemetriesData).then(\r\n function(response) {\r\n deferred.resolve(response.data);\r\n },\r\n function() {\r\n deferred.reject();\r\n }\r\n );\r\n }\r\n return deferred.promise;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n correctValue($scope.currentValue);\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n $scope.currentValue = 0;\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update double timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -189,13 +253,45 @@ "sizeX": 7.5, "sizeY": 3, "resources": [], - "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n Update server attribute\n \n \n close\n Discard changes\n \n \n \n\n \n No entity selected\n \n \n No timeseries is selected\n \n \n Attribute parameter cannot be used in this widget\n \n \n", + "templateHtml": "\n \n\n \n \n \n {{labelValue}}\n \n \n {{requiredErrorMessage}}\n \n \n \n\n \n \n check\n {{ 'widgets.input-widgets.update-timeseries' | translate }}\n \n \n close\n {{ 'widgets.input-widgets.discard-changes' | translate }}\n \n \n \n\n \n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n \n \n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n \n \n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $q;\nlet $http;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $q = $scope.$injector.get('$q');\n $http = $scope.$injector.get('$http');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\n $scope.labelValue = settings.labelValue || \"Value\";\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var deferred = $q.defer();\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n $http.post(url, telemetriesData).then(\n function(response) {\n deferred.resolve(response.data);\n },\n function() {\n deferred.reject();\n }\n );\n }\n return deferred.promise;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $translate;\nlet $q;\nlet $http;\n\nself.onInit = function() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get('attributeService');\n toast = $scope.$injector.get('toast');\n utils = $scope.$injector.get('utils');\n types = $scope.$injector.get('types');\n $translate = $scope.$injector.get('$translate');\n $q = $scope.$injector.get('$q');\n $http = $scope.$injector.get('$http');\n settings = angular.copy(self.ctx.settings) || {};\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || $translate.instant('widgets.input-widgets.value');\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === types.datasourceType.entity) {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type != types.dataKeyType.timeseries) {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.currentValue\n }\n ]\n ).then(\n function success() {\n $scope.originalValue = $scope.currentValue;\n if (settings.showResultMessage) {\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(self.ctx.$container), 'bottom left');\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(self.ctx.$container), 'bottom left');\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.currentValue === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var deferred = $q.defer();\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n $http.post(url, telemetriesData).then(\n function(response) {\n deferred.resolve(response.data);\n },\n function() {\n deferred.reject();\n }\n );\n }\n return deferred.promise;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n correctValue($scope.currentValue);\n $scope.$digest();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n $scope.currentValue = 0;\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } + }, + { + "alias": "update_multiple_attributes", + "name": "Update Multiple Attributes", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Multiple input title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributesShared\": {\n \"title\": \"Attributes are 'shared' (default value is 'server')\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"attributesShared\",\n \"showResultMessage\"\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"readOnly\": {\n \"title\": \"Value is read only\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"readOnly\",\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, + { + "alias": "web_camera_input", + "name": "Web Camera Input", + "descriptor": { + "type": "latest", + "sizeX": 9.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } } ] -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 98610082f9..5cfc2f9a19 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -16,6 +16,7 @@ package org.thingsboard.server.actors.ruleChain; import akka.actor.ActorRef; +import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -60,6 +61,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor; +import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -355,8 +357,8 @@ class DefaultTbContext implements TbContext { } @Override - public CassandraBufferedRateExecutor getCassandraBufferedRateExecutor() { - return mainCtx.getCassandraBufferedRateExecutor(); + public ResultSetFuture submitCassandraTask(CassandraStatementTask task) { + return mainCtx.getCassandraBufferedRateExecutor().submit(task); } private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { diff --git a/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java index b70f781acd..a27d09ab9a 100644 --- a/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SchedulingConfiguration.java @@ -23,9 +23,6 @@ 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 { diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index cc6ddcf73e..b44093a031 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -19,6 +19,7 @@ import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.TypeResolver; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Predicate; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.thingsboard.server.common.data.security.Authority; @@ -43,71 +44,94 @@ import static springfox.documentation.builders.PathSelectors.regex; @Configuration public class SwaggerConfiguration { - @Bean - public Docket thingsboardApi() { - TypeResolver typeResolver = new TypeResolver(); - final ResolvedType jsonNodeType = - typeResolver.resolve( - JsonNode.class); - final ResolvedType stringType = - typeResolver.resolve( - String.class); + @Value("${swagger.api_path_regex}") + private String apiPathRegex; + @Value("${swagger.security_path_regex}") + private String securityPathRegex; + @Value("${swagger.non_security_path_regex}") + private String nonSecurityPathRegex; + @Value("${swagger.title}") + private String title; + @Value("${swagger.description}") + private String description; + @Value("${swagger.contact.name}") + private String contactName; + @Value("${swagger.contact.url}") + private String contactUrl; + @Value("${swagger.contact.email}") + private String contactEmail; + @Value("${swagger.license.title}") + private String licenseTitle; + @Value("${swagger.license.url}") + private String licenseUrl; + @Value("${swagger.version}") + private String version; - return new Docket(DocumentationType.SWAGGER_2) - .groupName("thingsboard") - .apiInfo(apiInfo()) - .alternateTypeRules( + @Bean + public Docket thingsboardApi() { + TypeResolver typeResolver = new TypeResolver(); + final ResolvedType jsonNodeType = + typeResolver.resolve( + JsonNode.class); + final ResolvedType stringType = + typeResolver.resolve( + String.class); + + return new Docket(DocumentationType.SWAGGER_2) + .groupName("thingsboard") + .apiInfo(apiInfo()) + .alternateTypeRules( new AlternateTypeRule( jsonNodeType, stringType)) - .select() - .paths(apiPaths()) - .build() - .securitySchemes(newArrayList(jwtTokenKey())) - .securityContexts(newArrayList(securityContext())) - .enableUrlTemplating(true); - } + .select() + .paths(apiPaths()) + .build() + .securitySchemes(newArrayList(jwtTokenKey())) + .securityContexts(newArrayList(securityContext())) + .enableUrlTemplating(true); + } - private ApiKey jwtTokenKey() { - return new ApiKey("X-Authorization", "JWT token", "header"); - } + private ApiKey jwtTokenKey() { + return new ApiKey("X-Authorization", "JWT token", "header"); + } - private SecurityContext securityContext() { - return SecurityContext.builder() - .securityReferences(defaultAuth()) - .forPaths(securityPaths()) - .build(); - } + private SecurityContext securityContext() { + return SecurityContext.builder() + .securityReferences(defaultAuth()) + .forPaths(securityPaths()) + .build(); + } - private Predicate apiPaths() { - return regex("/api.*"); - } + private Predicate apiPaths() { + return regex(apiPathRegex); + } - private Predicate securityPaths() { - return and( - regex("/api.*"), - not(regex("/api/noauth.*")) - ); - } + private Predicate securityPaths() { + return and( + regex(securityPathRegex), + not(regex(nonSecurityPathRegex)) + ); + } - List defaultAuth() { - AuthorizationScope[] authorizationScopes = new AuthorizationScope[3]; - authorizationScopes[0] = new AuthorizationScope(Authority.SYS_ADMIN.name(), "System administrator"); - authorizationScopes[1] = new AuthorizationScope(Authority.TENANT_ADMIN.name(), "Tenant administrator"); - authorizationScopes[2] = new AuthorizationScope(Authority.CUSTOMER_USER.name(), "Customer"); - return newArrayList( - new SecurityReference("X-Authorization", authorizationScopes)); - } + List defaultAuth() { + AuthorizationScope[] authorizationScopes = new AuthorizationScope[3]; + authorizationScopes[0] = new AuthorizationScope(Authority.SYS_ADMIN.name(), "System administrator"); + authorizationScopes[1] = new AuthorizationScope(Authority.TENANT_ADMIN.name(), "Tenant administrator"); + authorizationScopes[2] = new AuthorizationScope(Authority.CUSTOMER_USER.name(), "Customer"); + return newArrayList( + new SecurityReference("X-Authorization", authorizationScopes)); + } - private ApiInfo apiInfo() { - return new ApiInfoBuilder() - .title("Thingsboard REST API") - .description("For instructions how to authorize requests please visit REST API documentation page.") - .contact(new Contact("Thingsboard team", "http://thingsboard.io", "info@thingsboard.io")) - .license("Apache License Version 2.0") - .licenseUrl("https://github.com/thingsboard/thingsboard/blob/master/LICENSE") - .version("2.0") + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title(title) + .description(description) + .contact(new Contact(contactName, contactUrl, contactEmail)) + .license(licenseTitle) + .licenseUrl(licenseUrl) + .version(version) .build(); - } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 4575678ab5..a6122dd82e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -28,8 +28,10 @@ import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.service.security.model.SecuritySettings; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.update.UpdateService; import org.thingsboard.server.service.update.model.UpdateMessage; @@ -43,6 +45,9 @@ public class AdminController extends BaseController { @Autowired private AdminSettingsService adminSettingsService; + @Autowired + private SystemSecurityService systemSecurityService; + @Autowired private UpdateService updateService; @@ -74,6 +79,31 @@ public class AdminController extends BaseController { } } + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/securitySettings", method = RequestMethod.GET) + @ResponseBody + public SecuritySettings getSecuritySettings() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + return checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/securitySettings", method = RequestMethod.POST) + @ResponseBody + public SecuritySettings saveSecuritySettings(@RequestBody SecuritySettings securitySettings) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); + securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings)); + return securitySettings; + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index 61fe7a384e..5047c42c70 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import org.apache.commons.lang3.StringUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -31,7 +33,10 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; +import java.util.Arrays; +import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @RestController @RequestMapping("/api") @@ -46,12 +51,14 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset) throws ThingsboardException { + @RequestParam(required = false) String offset, + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("CustomerId", strCustomerId); TenantId tenantId = getCurrentUser().getTenantId(); TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), pageLink)); + List actionTypes = parseActionTypesStr(actionTypesStr); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink)); } catch (Exception e) { throw handleException(e); } @@ -66,12 +73,14 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset) throws ThingsboardException { + @RequestParam(required = false) String offset, + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("UserId", strUserId); TenantId tenantId = getCurrentUser().getTenantId(); TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), pageLink)); + List actionTypes = parseActionTypesStr(actionTypesStr); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink)); } catch (Exception e) { throw handleException(e); } @@ -87,13 +96,15 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset) throws ThingsboardException { + @RequestParam(required = false) String offset, + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); TenantId tenantId = getCurrentUser().getTenantId(); TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), pageLink)); + List actionTypes = parseActionTypesStr(actionTypesStr); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink)); } catch (Exception e) { throw handleException(e); } @@ -107,13 +118,24 @@ public class AuditLogController extends BaseController { @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, @RequestParam(required = false, defaultValue = "false") boolean ascOrder, - @RequestParam(required = false) String offset) throws ThingsboardException { + @RequestParam(required = false) String offset, + @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, pageLink)); + List actionTypes = parseActionTypesStr(actionTypesStr); + return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink)); } catch (Exception e) { throw handleException(e); } } + + private List parseActionTypesStr(String actionTypesStr) { + List result = null; + if (StringUtils.isNoneBlank(actionTypesStr)) { + String[] tmp = actionTypesStr.split(","); + result = Arrays.stream(tmp).map(at -> ActionType.valueOf(at.toUpperCase())).collect(Collectors.toList()); + } + return result; + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index fdad8f09de..690ba76a0f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -24,6 +24,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -34,15 +35,24 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; +import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; +import org.thingsboard.server.service.security.model.SecuritySettings; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPasswordPolicy; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtToken; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; +import org.thingsboard.server.service.security.system.SystemSecurityService; +import ua_parser.Client; import javax.servlet.http.HttpServletRequest; import java.net.URI; @@ -65,6 +75,12 @@ public class AuthController extends BaseController { @Autowired private MailService mailService; + @Autowired + private SystemSecurityService systemSecurityService; + + @Autowired + private AuditLogService auditLogService; + @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/auth/user", method = RequestMethod.GET) public @ResponseBody User getUser() throws ThingsboardException { @@ -76,6 +92,13 @@ public class AuthController extends BaseController { } } + @PreAuthorize("isAuthenticated()") + @RequestMapping(value = "/auth/logout", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void logout(HttpServletRequest request) throws ThingsboardException { + logLogoutAction(request); + } + @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @@ -89,8 +112,24 @@ public class AuthController extends BaseController { if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } + systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword, userCredentials); + if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) { + throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } userCredentials.setPassword(passwordEncoder.encode(newPassword)); - userService.saveUserCredentials(securityUser.getTenantId(), userCredentials); + userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); + } catch (Exception e) { + throw handleException(e); + } + } + + @RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET) + @ResponseBody + public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException { + try { + SecuritySettings securitySettings = + checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID)); + return securitySettings.getPasswordPolicy(); } catch (Exception e) { throw handleException(e); } @@ -167,6 +206,7 @@ public class AuthController extends BaseController { try { String activateToken = activateRequest.get("activateToken").asText(); String password = activateRequest.get("password").asText(); + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, null); String encodedPassword = passwordEncoder.encode(password); UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword); User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId()); @@ -206,10 +246,14 @@ public class AuthController extends BaseController { String password = resetPasswordRequest.get("password").asText(); UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); if (userCredentials != null) { + systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials); + if (passwordEncoder.matches(password, userCredentials.getPassword())) { + throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } String encodedPassword = passwordEncoder.encode(password); userCredentials.setPassword(encodedPassword); userCredentials.setResetToken(null); - userCredentials = userService.saveUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); + userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials); User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId()); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal); @@ -234,4 +278,54 @@ public class AuthController extends BaseController { } } + private void logLogoutAction(HttpServletRequest request) throws ThingsboardException { + try { + SecurityUser user = getCurrentUser(); + RestAuthenticationDetails details = new RestAuthenticationDetails(request); + String clientAddress = details.getClientAddress(); + String browser = "Unknown"; + String os = "Unknown"; + String device = "Unknown"; + if (details.getUserAgent() != null) { + Client userAgent = details.getUserAgent(); + if (userAgent.userAgent != null) { + browser = userAgent.userAgent.family; + if (userAgent.userAgent.major != null) { + browser += " " + userAgent.userAgent.major; + if (userAgent.userAgent.minor != null) { + browser += "." + userAgent.userAgent.minor; + if (userAgent.userAgent.patch != null) { + browser += "." + userAgent.userAgent.patch; + } + } + } + } + if (userAgent.os != null) { + os = userAgent.os.family; + if (userAgent.os.major != null) { + os += " " + userAgent.os.major; + if (userAgent.os.minor != null) { + os += "." + userAgent.os.minor; + if (userAgent.os.patch != null) { + os += "." + userAgent.os.patch; + if (userAgent.os.patchMinor != null) { + os += "." + userAgent.os.patchMinor; + } + } + } + } + } + if (userAgent.device != null) { + device = userAgent.device.family; + } + } + auditLogService.logEntityAction( + user.getTenantId(), user.getCustomerId(), user.getId(), + user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); + + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 3753262bca..f497c4fefa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.controller.claim.data.ClaimRequest; import org.thingsboard.server.dao.device.claim.ClaimResponse; +import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.service.security.model.SecurityUser; @@ -406,19 +407,23 @@ public class DeviceController extends BaseController { device.getId(), device); String secretKey = getSecretKey(claimRequest); - ListenableFuture future = claimDevicesService.claimDevice(device, customerId, secretKey); - Futures.addCallback(future, new FutureCallback() { + ListenableFuture future = claimDevicesService.claimDevice(device, customerId, secretKey); + Futures.addCallback(future, new FutureCallback() { @Override - public void onSuccess(@Nullable ClaimResponse result) { + public void onSuccess(@Nullable ClaimResult result) { HttpStatus status; - if (result.equals(ClaimResponse.SUCCESS)) { - status = HttpStatus.OK; + if (result != null) { + if (result.getResponse().equals(ClaimResponse.SUCCESS)) { + status = HttpStatus.OK; + deferredResult.setResult(new ResponseEntity<>(result, status)); + } else { + status = HttpStatus.BAD_REQUEST; + deferredResult.setResult(new ResponseEntity<>(result.getResponse(), status)); + } } else { - status = HttpStatus.BAD_REQUEST; + deferredResult.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); } - deferredResult.setResult(new ResponseEntity<>(result, status)); } - @Override public void onFailure(Throwable t) { deferredResult.setErrorResult(t); diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index b6a8d9da06..135334a8c9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -126,7 +126,7 @@ public class UserController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user", method = RequestMethod.POST) - @ResponseBody + @ResponseBody public User saveUser(@RequestBody User user, @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException { @@ -285,5 +285,20 @@ public class UserController extends BaseController { throw handleException(e); } } - + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/user/{userId}/userCredentialsEnabled", method = RequestMethod.POST) + @ResponseBody + public void setUserCredentialsEnabled(@PathVariable(USER_ID) String strUserId, + @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException { + checkParameter(USER_ID, strUserId); + try { + UserId userId = new UserId(toUUID(strUserId)); + User user = checkUserId(userId, Operation.WRITE); + TenantId tenantId = getCurrentUser().getTenantId(); + userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java new file mode 100644 index 0000000000..c56d00a59e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java @@ -0,0 +1,37 @@ +/** + * 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.exception; + +import org.springframework.http.HttpStatus; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; + +public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse { + + private final String resetToken; + + protected ThingsboardCredentialsExpiredResponse(String message, String resetToken) { + super(message, ThingsboardErrorCode.CREDENTIALS_EXPIRED, HttpStatus.UNAUTHORIZED); + this.resetToken = resetToken; + } + + public static ThingsboardCredentialsExpiredResponse of(final String message, final String resetToken) { + return new ThingsboardCredentialsExpiredResponse(message, resetToken); + } + + public String getResetToken() { + return resetToken; + } +} diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index ee8d198f59..082f8fe0c1 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -22,15 +22,17 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; +import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -137,12 +139,21 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { response.setStatus(HttpStatus.UNAUTHORIZED.value()); if (authenticationException instanceof BadCredentialsException) { mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + } else if (authenticationException instanceof DisabledException) { + mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + } else if (authenticationException instanceof LockedException) { + mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof JwtExpiredTokenException) { mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof AuthMethodNotSupportedException) { mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + } else if (authenticationException instanceof UserPasswordExpiredException) { + UserPasswordExpiredException expiredException = (UserPasswordExpiredException)authenticationException; + String resetToken = expiredException.getResetToken(); + mapper.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken)); + } else { + mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } - mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } } diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 93449be05f..f384817465 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -23,11 +23,11 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.service.install.update.DataUpdateService; import org.thingsboard.server.service.install.DatabaseUpgradeService; import org.thingsboard.server.service.install.EntityDatabaseSchemaService; import org.thingsboard.server.service.install.SystemDataLoaderService; import org.thingsboard.server.service.install.TsDatabaseSchemaService; +import org.thingsboard.server.service.install.update.DataUpdateService; @Service @Profile("install") @@ -116,6 +116,9 @@ public class ThingsboardInstallService { databaseUpgradeService.upgradeDatabase("2.3.1"); + case "2.4.0": + log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ..."); + log.info("Updating system data..."); systemDataLoaderService.deleteSystemWidgetBundle("charts"); @@ -132,7 +135,6 @@ public class ThingsboardInstallService { systemDataLoaderService.deleteSystemWidgetBundle("date"); systemDataLoaderService.loadSystemWidgets(); - break; default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlTimescaleDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlTimescaleDatabaseSchemaService.java new file mode 100644 index 0000000000..23a335fe35 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlTimescaleDatabaseSchemaService.java @@ -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. + */ +package org.thingsboard.server.service.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.util.TimescaleDBTsDao; + +@Service +@TimescaleDBTsDao +@Profile("install") +public class SqlTimescaleDatabaseSchemaService extends SqlAbstractDatabaseSchemaService + implements TsDatabaseSchemaService { + public SqlTimescaleDatabaseSchemaService() { + super("schema-timescale.sql", "schema-timescale-idx.sql"); + } +} \ No newline at end of file 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 217d62b54e..d8781d876e 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 @@ -212,6 +212,21 @@ public class DefaultMailService implements MailService { mailSender.send(helper.getMimeMessage()); } + @Override + public void sendAccountLockoutEmail( String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException { + String subject = messages.getMessage("account.lockout.subject", null, Locale.US); + + Map model = new HashMap(); + model.put("lockoutAccount", lockoutEmail); + model.put("maxFailedLoginAttempts", maxFailedLoginAttempts); + model.put(TARGET_EMAIL, email); + + String message = mergeTemplateIntoString(this.engine, + "account.lockout.vm", UTF_8, model); + + sendMail(mailSender, mailFrom, email, subject, message); + } + private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email, String subject, String message) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java new file mode 100644 index 0000000000..5059cf800c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java @@ -0,0 +1,54 @@ +/** + * 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.service.security.auth.rest; + +import lombok.Data; +import ua_parser.Client; +import ua_parser.Parser; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.Serializable; + +@Data +public class RestAuthenticationDetails implements Serializable { + + private final String clientAddress; + private final Client userAgent; + + public RestAuthenticationDetails(HttpServletRequest request) { + this.clientAddress = getClientIP(request); + this.userAgent = getUserAgent(request); + } + + private static String getClientIP(HttpServletRequest request) { + String xfHeader = request.getHeader("X-Forwarded-For"); + if (xfHeader == null) { + return request.getRemoteAddr(); + } + return xfHeader.split(",")[0]; + } + + private static Client getUserAgent(HttpServletRequest request) { + try { + Parser uaParser = new Parser(); + return uaParser.parse(request.getHeader("User-Agent")); + } catch (IOException e) { + return new Client(null, null, null); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java new file mode 100644 index 0000000000..3983c46306 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetailsSource.java @@ -0,0 +1,29 @@ +/** + * 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.service.security.auth.rest; + +import org.springframework.security.authentication.AuthenticationDetailsSource; + +import javax.servlet.http.HttpServletRequest; + +public class RestAuthenticationDetailsSource implements + AuthenticationDetailsSource { + + public RestAuthenticationDetails buildDetails(HttpServletRequest context) { + return new RestAuthenticationDetails(context); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 72f9dd61fa..1e0f685622 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -15,45 +15,55 @@ */ package org.thingsboard.server.service.security.auth.rest; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.security.system.SystemSecurityService; +import ua_parser.Client; import java.util.UUID; @Component +@Slf4j public class RestAuthenticationProvider implements AuthenticationProvider { - private final BCryptPasswordEncoder encoder; + private final SystemSecurityService systemSecurityService; private final UserService userService; private final CustomerService customerService; + private final AuditLogService auditLogService; @Autowired - public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) { + public RestAuthenticationProvider(final UserService userService, + final CustomerService customerService, + final SystemSecurityService systemSecurityService, + final AuditLogService auditLogService) { this.userService = userService; this.customerService = customerService; - this.encoder = encoder; + this.systemSecurityService = systemSecurityService; + this.auditLogService = auditLogService; } @Override @@ -69,37 +79,43 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { String username = userPrincipal.getValue(); String password = (String) authentication.getCredentials(); - return authenticateByUsernameAndPassword(userPrincipal, username, password); + return authenticateByUsernameAndPassword(authentication, userPrincipal, username, password); } else { String publicId = userPrincipal.getValue(); return authenticateByPublicId(userPrincipal, publicId); } } - private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) { + private Authentication authenticateByUsernameAndPassword(Authentication authentication, UserPrincipal userPrincipal, String username, String password) { User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username); if (user == null) { throw new UsernameNotFoundException("User not found: " + username); } - UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId()); - if (userCredentials == null) { - throw new UsernameNotFoundException("User credentials not found"); - } - - if (!userCredentials.isEnabled()) { - throw new DisabledException("User is not active"); - } + try { - if (!encoder.matches(password, userCredentials.getPassword())) { - throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); - } + UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId()); + if (userCredentials == null) { + throw new UsernameNotFoundException("User credentials not found"); + } - if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); + try { + systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password); + } catch (LockedException e) { + logLoginAction(user, authentication, ActionType.LOCKOUT, null); + throw e; + } - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + if (user.getAuthority() == null) + throw new InsufficientAuthenticationException("User has no authority assigned"); - return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + logLoginAction(user, authentication, ActionType.LOGIN, null); + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); + } catch (Exception e) { + logLoginAction(user, authentication, ActionType.LOGIN, e); + throw e; + } } private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { @@ -133,4 +149,53 @@ public class RestAuthenticationProvider implements AuthenticationProvider { public boolean supports(Class> authentication) { return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); } + + private void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e) { + String clientAddress = "Unknown"; + String browser = "Unknown"; + String os = "Unknown"; + String device = "Unknown"; + if (authentication != null && authentication.getDetails() != null) { + if (authentication.getDetails() instanceof RestAuthenticationDetails) { + RestAuthenticationDetails details = (RestAuthenticationDetails)authentication.getDetails(); + clientAddress = details.getClientAddress(); + if (details.getUserAgent() != null) { + Client userAgent = details.getUserAgent(); + if (userAgent.userAgent != null) { + browser = userAgent.userAgent.family; + if (userAgent.userAgent.major != null) { + browser += " " + userAgent.userAgent.major; + if (userAgent.userAgent.minor != null) { + browser += "." + userAgent.userAgent.minor; + if (userAgent.userAgent.patch != null) { + browser += "." + userAgent.userAgent.patch; + } + } + } + } + if (userAgent.os != null) { + os = userAgent.os.family; + if (userAgent.os.major != null) { + os += " " + userAgent.os.major; + if (userAgent.os.minor != null) { + os += "." + userAgent.os.minor; + if (userAgent.os.patch != null) { + os += "." + userAgent.os.patch; + if (userAgent.os.patchMinor != null) { + os += "." + userAgent.os.patchMinor; + } + } + } + } + } + if (userAgent.device != null) { + device = userAgent.device.family; + } + } + } + } + auditLogService.logEntityAction( + user.getTenantId(), user.getCustomerId(), user.getId(), + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java index f233d5c516..77f7908b03 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -27,6 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -39,6 +41,8 @@ import java.io.IOException; @Slf4j public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { + private final AuthenticationDetailsSource authenticationDetailsSource = new RestAuthenticationDetailsSource(); + private final AuthenticationSuccessHandler successHandler; private final AuthenticationFailureHandler failureHandler; @@ -76,7 +80,7 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword()); - + token.setDetails(authenticationDetailsSource.buildDetails(request)); return this.getAuthenticationManager().authenticate(token); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java new file mode 100644 index 0000000000..993d09bbf0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordExpiredException.java @@ -0,0 +1,33 @@ +/** + * 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.service.security.exception; + +import org.springframework.security.authentication.CredentialsExpiredException; + +public class UserPasswordExpiredException extends CredentialsExpiredException { + + private final String resetToken; + + public UserPasswordExpiredException(String msg, String resetToken) { + super(msg); + this.resetToken = resetToken; + } + + public String getResetToken() { + return resetToken; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java new file mode 100644 index 0000000000..bfc9176b4b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java @@ -0,0 +1,29 @@ +/** + * 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.service.security.model; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class SecuritySettings implements Serializable { + + private UserPasswordPolicy passwordPolicy; + + private Integer maxFailedLoginAttempts; + private String userLockoutNotificationEmail; +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java b/application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java new file mode 100644 index 0000000000..0c50b5f6e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java @@ -0,0 +1,34 @@ +/** + * 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.service.security.model; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class UserPasswordPolicy implements Serializable { + + private Integer minimumLength; + private Integer minimumUppercaseLetters; + private Integer minimumLowercaseLetters; + private Integer minimumDigits; + private Integer minimumSpecialCharacters; + + private Integer passwordExpirationPeriodDays; + private Integer passwordReuseFrequencyDays; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java new file mode 100644 index 0000000000..94d0fea86f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -0,0 +1,204 @@ +/** + * 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.service.security.system; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.LengthRule; +import org.passay.PasswordData; +import org.passay.PasswordValidator; +import org.passay.Rule; +import org.passay.RuleResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.user.UserServiceImpl; +import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; +import org.thingsboard.server.service.security.model.SecuritySettings; +import org.thingsboard.server.service.security.model.UserPasswordPolicy; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE; + +@Service +@Slf4j +public class DefaultSystemSecurityService implements SystemSecurityService { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private AdminSettingsService adminSettingsService; + + @Autowired + private BCryptPasswordEncoder encoder; + + @Autowired + private UserService userService; + + @Autowired + private MailService mailService; + + @Resource + private SystemSecurityService self; + + @Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'") + @Override + public SecuritySettings getSecuritySettings(TenantId tenantId) { + SecuritySettings securitySettings = null; + AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings"); + if (adminSettings != null) { + try { + securitySettings = objectMapper.treeToValue(adminSettings.getJsonValue(), SecuritySettings.class); + } catch (Exception e) { + throw new RuntimeException("Failed to load security settings!", e); + } + } else { + securitySettings = new SecuritySettings(); + securitySettings.setPasswordPolicy(new UserPasswordPolicy()); + securitySettings.getPasswordPolicy().setMinimumLength(6); + } + return securitySettings; + } + + @CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'") + @Override + public SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings) { + AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings"); + if (adminSettings == null) { + adminSettings = new AdminSettings(); + adminSettings.setKey("securitySettings"); + } + adminSettings.setJsonValue(objectMapper.valueToTree(securitySettings)); + AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings); + try { + return objectMapper.treeToValue(savedAdminSettings.getJsonValue(), SecuritySettings.class); + } catch (Exception e) { + throw new RuntimeException("Failed to load security settings!", e); + } + } + + @Override + public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException { + if (!encoder.matches(password, userCredentials.getPassword())) { + int failedLoginAttempts = userService.onUserLoginIncorrectCredentials(tenantId, userCredentials.getUserId()); + SecuritySettings securitySettings = getSecuritySettings(tenantId); + if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { + if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { + userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userCredentials.getUserId(), false); + if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) { + try { + mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); + } catch (ThingsboardException e) { + log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e); + } + } + throw new LockedException("Authentication Failed. Username was locked due to security policy."); + } + } + throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); + } + + if (!userCredentials.isEnabled()) { + throw new DisabledException("User is not active"); + } + + userService.onUserLoginSuccessful(tenantId, userCredentials.getUserId()); + + SecuritySettings securitySettings = self.getSecuritySettings(tenantId); + if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { + if ((userCredentials.getCreatedTime() + + TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) + < System.currentTimeMillis()) { + userCredentials = userService.requestExpiredPasswordReset(tenantId, userCredentials.getId()); + throw new UserPasswordExpiredException("User password expired!", userCredentials.getResetToken()); + } + } + } + + @Override + public void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException { + SecuritySettings securitySettings = self.getSecuritySettings(tenantId); + UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); + + List passwordRules = new ArrayList<>(); + passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), Integer.MAX_VALUE)); + if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) { + passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters())); + } + if (isPositiveInteger(passwordPolicy.getMinimumLowercaseLetters())) { + passwordRules.add(new CharacterRule(EnglishCharacterData.LowerCase, passwordPolicy.getMinimumLowercaseLetters())); + } + if (isPositiveInteger(passwordPolicy.getMinimumDigits())) { + passwordRules.add(new CharacterRule(EnglishCharacterData.Digit, passwordPolicy.getMinimumDigits())); + } + if (isPositiveInteger(passwordPolicy.getMinimumSpecialCharacters())) { + passwordRules.add(new CharacterRule(EnglishCharacterData.Special, passwordPolicy.getMinimumSpecialCharacters())); + } + PasswordValidator validator = new PasswordValidator(passwordRules); + PasswordData passwordData = new PasswordData(password); + RuleResult result = validator.validate(passwordData); + if (!result.isValid()) { + String message = String.join("\n", validator.getMessages(result)); + throw new DataValidationException(message); + } + + if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) { + long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays()); + User user = userService.findUserById(tenantId, userCredentials.getUserId()); + JsonNode additionalInfo = user.getAdditionalInfo(); + if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { + JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); + Map userPasswordHistoryMap = objectMapper.convertValue(userPasswordHistoryJson, Map.class); + for (Map.Entry entry : userPasswordHistoryMap.entrySet()) { + if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) { + throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days"); + } + } + + } + } + } + + private static boolean isPositiveInteger(Integer val) { + return val != null && val.intValue() > 0; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java new file mode 100644 index 0000000000..50265863b3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -0,0 +1,34 @@ +/** + * 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.service.security.system; + +import org.springframework.security.core.AuthenticationException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.service.security.model.SecuritySettings; + +public interface SystemSecurityService { + + SecuritySettings getSecuritySettings(TenantId tenantId); + + SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings); + + void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException; + + void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index f2b51ec2ca..c4ddcf654b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -25,7 +25,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; -import org.thingsboard.rule.engine.api.util.DonAsynchron; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; diff --git a/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java index 4dea106958..044a2a15f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/LocalTransportService.java @@ -20,7 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.util.DonAsynchron; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/resources/i18n/messages.properties b/application/src/main/resources/i18n/messages.properties index a78fbe07b3..a34582e132 100644 --- a/application/src/main/resources/i18n/messages.properties +++ b/application/src/main/resources/i18n/messages.properties @@ -3,3 +3,4 @@ activation.subject=Your account activation on Thingsboard account.activated.subject=Thingsboard - your account has been activated reset.password.subject=Thingsboard - Password reset has been requested password.was.reset.subject=Thingsboard - your account password has been reset +account.lockout.subject=Thingsboard - User account has been lockout \ No newline at end of file diff --git a/application/src/main/resources/templates/account.lockout.vm b/application/src/main/resources/templates/account.lockout.vm new file mode 100644 index 0000000000..9b735c8998 --- /dev/null +++ b/application/src/main/resources/templates/account.lockout.vm @@ -0,0 +1,112 @@ +#* + * 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. + *# + + + + + +Thingsboard - Account Lockout + + + + + + + + + + + + + + + Thingsboard user account has been locked out + + + + + Thingsboard user account $lockoutAccount has been lockout due to failed credentials were provided more than $maxFailedLoginAttempts times. + + + + + — The Thingsboard + + + + + + + + + + + + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 0e291db2cc..48e37dd646 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -119,8 +119,9 @@ database: entities: type: "${DATABASE_ENTITIES_TYPE:sql}" # cassandra OR sql ts: - type: "${DATABASE_TS_TYPE:sql}" # cassandra OR sql (for hybrid mode, only this value should be cassandra) + type: "${DATABASE_TS_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) +# note: timescale works only with postgreSQL database for DATABASE_ENTITIES_TYPE. # Cassandra driver configuration parameters cassandra: @@ -189,10 +190,10 @@ cassandra: # SQL configuration parameters sql: - # Specify executor service type used to perform timeseries insert tasks: SINGLE FIXED CACHED + # Specify executor service type used to perform timeseries insert tasks: SINGLE or FIXED ts_inserts_executor_type: "${SQL_TS_INSERTS_EXECUTOR_TYPE:fixed}" # Specify thread pool size for FIXED executor service type - ts_inserts_fixed_thread_pool_size: "${SQL_TS_INSERTS_FIXED_THREAD_POOL_SIZE:10}" + ts_inserts_fixed_thread_pool_size: "${SQL_TS_INSERTS_FIXED_THREAD_POOL_SIZE:200}" # Actor system parameters actors: @@ -221,7 +222,7 @@ actors: error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" debug_mode_rate_limits_per_tenant: enabled: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}" - configuration: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:500:3600}" + configuration: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" node: # Errors for particular actor are persisted once per specified amount of milliseconds error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}" @@ -269,6 +270,9 @@ caffeine: claimDevices: timeToLiveInMinutes: 1 maxSize: 100000 + securitySettings: + timeToLiveInMinutes: 1440 + maxSize: 1 redis: # standalone or cluster @@ -324,6 +328,8 @@ spring: url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}" username: "${SPRING_DATASOURCE_USERNAME:postgres}" password: "${SPRING_DATASOURCE_PASSWORD:postgres}" + hikari: + maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:50}" # Audit log parameters audit-log: @@ -485,3 +491,18 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" + +swagger: + api_path_regex: "${SWAGGER_API_PATH_REGEX:/api.*}" + security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api.*}" + non_security_path_regex: "${SWAGGER_NON_SECURITY_PATH_REGEX:/api/noauth.*}" + title: "${SWAGGER_TITLE:Thingsboard REST API}" + description: "${SWAGGER_DESCRIPTION:For instructions how to authorize requests please visit REST API documentation page.}" + contact: + name: "${SWAGGER_CONTACT_NAME:Thingsboard team}" + url: "${SWAGGER_CONTACT_URL:http://thingsboard.io}" + email: "${SWAGGER_CONTACT_EMAIL:info@thingsboard.io}" + license: + title: "${SWAGGER_LICENSE_TITLE:Apache License Version 2.0}" + url: "${SWAGGER_LICENSE_URL:https://github.com/thingsboard/thingsboard/blob/master/LICENSE}" + version: "${SWAGGER_VERSION:2.0}" \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java index 72b8ccf89a..696d836f42 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java @@ -105,18 +105,6 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString("Provided json structure is different"))); } - @Test - public void testSaveAdminSettingsWithNonTextValue() throws Exception { - loginSysAdmin(); - AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); - JsonNode json = adminSettings.getJsonValue(); - ((ObjectNode) json).put("timeout", 10000L); - adminSettings.setJsonValue(json); - doPost("/api/admin/settings", adminSettings) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Provided json structure can't contain non-text values"))); - } - @Test public void testSendTestMail() throws Exception { loginSysAdmin(); diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml new file mode 100644 index 0000000000..a14271896e --- /dev/null +++ b/common/dao-api/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + org.thingsboard + 2.4.1 + common + + org.thingsboard.common + dao-api + jar + + Thingsboard Server Common DAO API + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + data + + + org.thingsboard.common + message + + + com.google.guava + guava + + + com.github.fge + json-schema-validator + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + com.datastax.cassandra + cassandra-driver-core + provided + + + com.datastax.cassandra + cassandra-driver-mapping + provided + + + com.datastax.cassandra + cassandra-driver-extras + provided + + + org.apache.commons + commons-lang3 + provided + + + junit + junit + test + + + org.mockito + mockito-all + test + + + + + + + + + diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java similarity index 85% rename from dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java index 90557dbabc..764ff806a3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java @@ -32,13 +32,13 @@ import java.util.List; public interface AuditLogService { - TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink); + TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink); - TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink); + TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink); - TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink); + TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink); - TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink); + TimePageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink); ListenableFuture> logEntityAction( TenantId tenantId, diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/event/EventService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/CassandraStatementTask.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java similarity index 82% rename from dao/src/main/java/org/thingsboard/server/dao/user/UserService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index fe53b09bf6..f5abe74f67 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; @@ -46,6 +47,10 @@ public interface UserService { UserCredentials requestPasswordReset(TenantId tenantId, String email); + UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId); + + UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); + void deleteUser(TenantId tenantId, UserId userId); TextPageData findTenantAdmins(TenantId tenantId, TextPageLink pageLink); @@ -55,5 +60,10 @@ public interface UserService { TextPageData findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); - + + void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled); + + void onUserLoginSuccessful(TenantId tenantId, UserId userId); + + int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AsyncTask.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/AsyncTask.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/util/AsyncTask.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/util/AsyncTask.java diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/HsqlDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/HsqlDao.java new file mode 100644 index 0000000000..b5f21dcdaf --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/HsqlDao.java @@ -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. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +@ConditionalOnProperty(prefix = "spring.jpa", value = "database-platform", havingValue = "org.hibernate.dialect.HSQLDialect") +public @interface HsqlDao { +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlTsDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlTsDao.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/util/NoSqlTsDao.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/util/NoSqlTsDao.java diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlDao.java new file mode 100644 index 0000000000..b540f3e97b --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlDao.java @@ -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. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +@ConditionalOnProperty(prefix = "spring.jpa", value = "database-platform", havingValue = "org.hibernate.dialect.PostgreSQLDialect") +public @interface PsqlDao { +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/SqlDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlDao.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/util/SqlDao.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlDao.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/SqlTsDao.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsDao.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/util/SqlTsDao.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsDao.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java diff --git a/common/data/pom.xml b/common/data/pom.xml index fee3817a12..65e2b9a1a7 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.0 + 2.4.1 common org.thingsboard.common diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 67f0fb9a0b..f15997936c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -23,4 +23,5 @@ public class CacheConstants { public static final String ASSET_CACHE = "assets"; public static final String ENTITY_VIEW_CACHE = "entityViews"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; + public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java index 252e3bcc79..d6dd206b3f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java @@ -19,7 +19,7 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.UUIDBased; @EqualsAndHashCode(callSuper = true) -public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo { +public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasName { private static final long serialVersionUID = 5047448057830660988L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index e6e5ffbdd3..a072e48555 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId; import com.fasterxml.jackson.databind.JsonNode; -public class Customer extends ContactBased implements HasName, HasTenantId { +public class Customer extends ContactBased implements HasTenantId { private static final long serialVersionUID = -1599722990298929275L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index 90df5ebc5a..bc6027418c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId; import com.fasterxml.jackson.databind.JsonNode; @EqualsAndHashCode(callSuper = true) -public class Tenant extends ContactBased implements HasName, HasTenantId { +public class Tenant extends ContactBased implements HasTenantId { private static final long serialVersionUID = 8057243243859922101L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 84eaf99fa3..77357d2294 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -37,7 +37,10 @@ public enum ActionType { RELATION_DELETED(false), RELATIONS_DELETED(false), ALARM_ACK(false), - ALARM_CLEAR(false); + ALARM_CLEAR(false), + LOGIN(false), + LOGOUT(false), + LOCKOUT(false); private final boolean isRead; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index e11e96ac0c..1393712de8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -22,6 +22,7 @@ public enum ThingsboardErrorCode { GENERAL(2), AUTHENTICATION(10), JWT_TOKEN_EXPIRED(11), + CREDENTIALS_EXPIRED(15), PERMISSION_DENIED(20), INVALID_ARGUMENTS(30), BAD_REQUEST_PARAMS(31), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java index 840eb2a529..9963d3e75b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java @@ -35,17 +35,19 @@ public class RelationsSearchParameters { private EntitySearchDirection direction; private RelationTypeGroup relationTypeGroup; private int maxLevel = 1; + private boolean fetchLastLevelOnly; - public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel) { - this(entityId, direction, maxLevel, RelationTypeGroup.COMMON); + public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel, boolean fetchLastLevelOnly) { + this(entityId, direction, maxLevel, RelationTypeGroup.COMMON, fetchLastLevelOnly); } - public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel, RelationTypeGroup relationTypeGroup) { + public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel, RelationTypeGroup relationTypeGroup, boolean fetchLastLevelOnly) { this.rootId = entityId.getId(); this.rootType = entityId.getEntityType(); this.direction = direction; this.maxLevel = maxLevel; this.relationTypeGroup = relationTypeGroup; + this.fetchLastLevelOnly = fetchLastLevelOnly; } public EntityId getEntityId() { diff --git a/common/message/pom.xml b/common/message/pom.xml index 9fb1bfc3da..e8cbadd99a 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.0 + 2.4.1 common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index cf915f725b..84b415d946 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.0 + 2.4.1 thingsboard common @@ -35,9 +35,11 @@ data + util message queue transport + dao-api diff --git a/common/queue/pom.xml b/common/queue/pom.xml index db8e6a1d9a..084c988dd9 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.0 + 2.4.1 common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 6c961e9423..84ff3a332f 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.4.0 + 2.4.1 transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index be7161504b..e425aaa718 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.4.0 + 2.4.1 transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 54489a219d..61dec2a412 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.4.0 + 2.4.1 transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index d7d2281b4f..d92b04e584 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttSubscribeMessage; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; import io.netty.handler.ssl.SslHandler; +import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; @@ -112,10 +113,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { log.trace("[{}] Processing msg: {}", sessionId, msg); - if (msg instanceof MqttMessage) { - processMqttMsg(ctx, (MqttMessage) msg); - } else { - ctx.close(); + try { + if (msg instanceof MqttMessage) { + processMqttMsg(ctx, (MqttMessage) msg); + } else { + ctx.close(); + } + } finally { + ReferenceCountUtil.safeRelease(msg); } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index db2c2193bc..990711a2f8 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -212,18 +212,14 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } private static String validatePayload(UUID sessionId, ByteBuf payloadData, boolean isEmptyPayloadAllowed) throws AdaptorException { - try { - String payload = payloadData.toString(UTF8); - if (payload == null) { - log.warn("[{}] Payload is empty!", sessionId); - if (!isEmptyPayloadAllowed) { - throw new AdaptorException(new IllegalArgumentException("Payload is empty!")); - } + String payload = payloadData.toString(UTF8); + if (payload == null) { + log.warn("[{}] Payload is empty!", sessionId); + if (!isEmptyPayloadAllowed) { + throw new AdaptorException(new IllegalArgumentException("Payload is empty!")); } - return payload; - } finally { - payloadData.release(); } + return payload; } } diff --git a/common/transport/pom.xml b/common/transport/pom.xml index c8b67bfb53..9aeec204fa 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.0 + 2.4.1 common org.thingsboard.common diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index e1c945eb7a..72e81b9684 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 2.4.0 + 2.4.1 transport org.thingsboard.common.transport diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java index 239a461cb1..d8912c18fb 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java @@ -200,7 +200,7 @@ public abstract class AbstractTransportService implements TransportService { @Override public void deregisterSession(TransportProtos.SessionInfoProto sessionInfo) { SessionMetaData currentSession = sessions.get(toId(sessionInfo)); - if (currentSession.hasScheduledFuture()) { + if (currentSession != null && currentSession.hasScheduledFuture()) { log.debug("Stopping scheduler to avoid resending response if request has been ack."); currentSession.getScheduledFuture().cancel(false); } diff --git a/common/util/pom.xml b/common/util/pom.xml new file mode 100644 index 0000000000..1a3beff12c --- /dev/null +++ b/common/util/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + org.thingsboard + 2.4.1 + common + + org.thingsboard.common + util + jar + + Thingsboard Server Common Utils + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + com.google.guava + guava + provided + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + junit + junit + test + + + org.mockito + mockito-all + test + + + + + + + + + diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java similarity index 93% rename from rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java rename to common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java index 3e4800d44e..75d58fd0ac 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.api.util; +package org.thingsboard.common.util; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import javax.annotation.Nullable; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -34,7 +33,7 @@ public class DonAsynchron { Consumer onFailure, Executor executor) { FutureCallback callback = new FutureCallback() { @Override - public void onSuccess(@Nullable T result) { + public void onSuccess(T result) { try { onSuccess.accept(result); } catch (Throwable th) { diff --git a/dao/pom.xml b/dao/pom.xml index 1fc94e86f4..a47aafbc0a 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.0 + 2.4.1 thingsboard dao @@ -43,6 +43,10 @@ org.thingsboard.common message + + org.thingsboard.common + dao-api + org.slf4j slf4j-api @@ -95,10 +99,6 @@ com.fasterxml.jackson.core jackson-databind - - com.github.fge - json-schema-validator - org.springframework spring-context diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java index 5805eeaccf..58019f9208 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java @@ -22,6 +22,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.util.TimescaleDBTsDao; /** * @author Valerii Sosliuk diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java new file mode 100644 index 0000000000..bc37316dda --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java @@ -0,0 +1,35 @@ +/** + * 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.dao; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.SqlTsDao; + +@Configuration +@EnableAutoConfiguration +@ComponentScan("org.thingsboard.server.dao.sqlts.ts") +@EnableJpaRepositories("org.thingsboard.server.dao.sqlts.ts") +@EntityScan("org.thingsboard.server.dao.model.sqlts.ts") +@EnableTransactionManagement +@SqlTsDao +public class SqlTsDaoConfig { + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java new file mode 100644 index 0000000000..87982203a4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java @@ -0,0 +1,35 @@ +/** + * 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.dao; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.TimescaleDBTsDao; + +@Configuration +@EnableAutoConfiguration +@ComponentScan("org.thingsboard.server.dao.sqlts.timescale") +@EnableJpaRepositories("org.thingsboard.server.dao.sqlts.timescale") +@EntityScan("org.thingsboard.server.dao.model.sqlts.timescale") +@EnableTransactionManagement +@TimescaleDBTsDao +public class TimescaleDaoConfig { + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 3d81dafb5c..d83b068cbe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -153,7 +153,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ private List getParentEntities(Alarm alarm) throws InterruptedException, ExecutionException { EntityRelationsQuery query = new EntityRelationsQuery(); - query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE)); + query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE, false)); return relationService.findByQuery(alarm.getTenantId(), query).get().stream().map(EntityRelation::getFrom).collect(Collectors.toList()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java index 2d8db71ab7..b00edf2241 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.audit; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -37,11 +38,11 @@ public interface AuditLogDao { ListenableFuture savePartitionsByTenantId(AuditLog auditLog); - List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink); + List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink); - List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink); + List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink); - List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink); + List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink); - List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink); + List findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink); } 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 ad3db8781f..436415d9d9 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 @@ -81,37 +81,37 @@ public class AuditLogServiceImpl implements AuditLogService { private AuditLogSink auditLogSink; @Override - public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, "Incorrect customerId " + customerId); - List auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, pageLink); + List auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, actionTypes, pageLink); return new TimePageData<>(auditLogs, pageLink); } @Override - public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(userId, "Incorrect userId" + userId); - List auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, pageLink); + List auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, actionTypes, pageLink); return new TimePageData<>(auditLogs, pageLink); } @Override - public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateEntityId(entityId, INCORRECT_TENANT_ID + entityId); - List auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, pageLink); + List auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, actionTypes, pageLink); return new TimePageData<>(auditLogs, pageLink); } @Override - public TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { log.trace("Executing findAuditLogs [{}]", pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - List auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), pageLink); + List auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), actionTypes, pageLink); return new TimePageData<>(auditLogs, pageLink); } @@ -248,6 +248,18 @@ public class AuditLogServiceImpl implements AuditLogService { EntityRelation relation = extractParameter(EntityRelation.class, 0, additionalInfo); actionData.set("relation", objectMapper.valueToTree(relation)); break; + case LOGIN: + case LOGOUT: + case LOCKOUT: + String clientAddress = extractParameter(String.class, 0, additionalInfo); + String browser = extractParameter(String.class, 1, additionalInfo); + String os = extractParameter(String.class, 2, additionalInfo); + String device = extractParameter(String.class, 3, additionalInfo); + actionData.put("clientAddress", clientAddress); + actionData.put("browser", browser); + actionData.put("os", os); + actionData.put("device", device); + break; } return actionData; } 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 6ffaf1ea00..675a24a7f7 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 @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -273,7 +274,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { + public List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); List entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_ENTITY_ID_CF, Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), @@ -285,7 +286,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { + public List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); List entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_CUSTOMER_ID_CF, Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), @@ -296,7 +297,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { + public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); List entities = findPageWithTimeSearch(new TenantId(tenantId), AUDIT_LOG_BY_USER_ID_CF, Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), @@ -307,7 +308,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { + public List findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); long minPartition; diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java index b8fabd5f23..304bd8feb0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java @@ -37,22 +37,22 @@ import java.util.List; public class DummyAuditLogServiceImpl implements AuditLogService { @Override - public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { return new TimePageData<>(null, pageLink); } @Override - public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { return new TimePageData<>(null, pageLink); } @Override - public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { return new TimePageData<>(null, pageLink); } @Override - public TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { + public TimePageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { return new TimePageData<>(null, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java index 75c0c2fb01..f1c4b5a8fc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java @@ -19,37 +19,18 @@ import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; -import org.springframework.cache.interceptor.SimpleKey; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.core.convert.converter.Converter; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.ConverterRegistry; -import org.springframework.data.convert.ReadingConverter; -import org.springframework.data.convert.WritingConverter; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.convert.RedisCustomConversions; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; -import java.util.UUID; @Configuration @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false) diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java index dad22a4a06..eb8c800d1e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java @@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.device.claim.ClaimResponse; +import org.thingsboard.server.dao.device.claim.ClaimResult; import java.util.List; @@ -28,7 +28,7 @@ public interface ClaimDevicesService { ListenableFuture registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs); - ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey); + ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey); ListenableFuture> reClaimDevice(TenantId tenantId, Device device); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java index 76a73c37d2..9000c91823 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.claim.ClaimData; import org.thingsboard.server.dao.device.claim.ClaimResponse; +import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.dao.model.ModelConstants; import java.util.Collections; @@ -95,7 +96,7 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { } @Override - public ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) { + public ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) { List key = constructCacheKey(device.getId()); Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); ClaimData claimData = cache.get(key, ClaimData.class); @@ -104,18 +105,22 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { if (currTs > claimData.getExpirationTime() || !secretKey.equals(claimData.getSecretKey())) { log.warn("The claiming timeout occurred or wrong 'secretKey' provided for the device [{}]", device.getName()); cache.evict(key); - return Futures.immediateFuture(ClaimResponse.FAILURE); + return Futures.immediateFuture(new ClaimResult(null, ClaimResponse.FAILURE)); } else { if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { device.setCustomerId(customerId); - deviceService.saveDevice(device); - return Futures.transform(removeClaimingSavedData(cache, key, device), result -> ClaimResponse.SUCCESS); + Device savedDevice = deviceService.saveDevice(device); + return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(savedDevice, ClaimResponse.SUCCESS)); } - return Futures.transform(removeClaimingSavedData(cache, key, device), result -> ClaimResponse.CLAIMED); + return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(null, ClaimResponse.CLAIMED)); } } else { log.warn("Failed to find the device's claiming message![{}]", device.getName()); - return Futures.immediateFuture(ClaimResponse.CLAIMED); + if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + return Futures.immediateFuture(new ClaimResult(null, ClaimResponse.FAILURE)); + } else { + return Futures.immediateFuture(new ClaimResult(null, ClaimResponse.CLAIMED)); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java b/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java new file mode 100644 index 0000000000..ba740a1bcb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java @@ -0,0 +1,30 @@ +/** + * 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.dao.device.claim; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.Device; + +@AllArgsConstructor +@Data +public class ClaimResult { + + private Device device; + private ClaimResponse response; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index bf1c56e92e..5a9575ff79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -27,7 +27,7 @@ import java.util.Optional; import java.util.UUID; /** - * The Interface DeviceDao. + * The Interface EventDao. */ public interface EventDao extends Dao { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index d9cb365a14..f065443489 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -47,6 +47,7 @@ public class ModelConstants { public static final String ENTITY_TYPE_PROPERTY = "entity_type"; public static final String ENTITY_TYPE_COLUMN = ENTITY_TYPE_PROPERTY; + public static final String TENANT_ID_COLUMN = "tenant_id"; public static final String ENTITY_ID_COLUMN = "entity_id"; public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type"; public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java new file mode 100644 index 0000000000..2773e5e4ab --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java @@ -0,0 +1,98 @@ +/** + * 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.dao.model.sql; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.model.ToData; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; + +@Data +@MappedSuperclass +public abstract class AbsractTsKvEntity implements ToData { + + protected static final String SUM = "SUM"; + protected static final String AVG = "AVG"; + protected static final String MIN = "MIN"; + protected static final String MAX = "MAX"; + + @Id + @Column(name = ENTITY_ID_COLUMN) + protected String entityId; + + @Id + @Column(name = TS_COLUMN) + protected Long ts; + + @Id + @Column(name = KEY_COLUMN) + protected String key; + + @Column(name = BOOLEAN_VALUE_COLUMN) + protected Boolean booleanValue; + + @Column(name = STRING_VALUE_COLUMN) + protected String strValue; + + @Column(name = LONG_VALUE_COLUMN) + protected Long longValue; + + @Column(name = DOUBLE_VALUE_COLUMN) + protected Double doubleValue; + + @Override + public TsKvEntry toData() { + KvEntry kvEntry = null; + if (strValue != null) { + kvEntry = new StringDataEntry(key, strValue); + } else if (longValue != null) { + kvEntry = new LongDataEntry(key, longValue); + } else if (doubleValue != null) { + kvEntry = new DoubleDataEntry(key, doubleValue); + } else if (booleanValue != null) { + kvEntry = new BooleanDataEntry(key, booleanValue); + } + return new BasicTsKvEntry(ts, kvEntry); + } + + public abstract boolean isNotEmpty(); + + protected static boolean isAllNull(Object... args) { + for (Object arg : args) { + if(arg != null) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java index 74aed9044c..5ae338f5c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.model.sql; import lombok.Data; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -29,19 +28,11 @@ import org.thingsboard.server.dao.model.ToData; import javax.persistence.Column; import javax.persistence.EmbeddedId; import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.Id; -import javax.persistence.IdClass; import javax.persistence.Table; import java.io.Serializable; -import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_KEY_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvCompositeKey.java new file mode 100644 index 0000000000..a8e7494627 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvCompositeKey.java @@ -0,0 +1,37 @@ +/** + * 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.dao.model.sqlts.timescale; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Transient; +import java.io.Serializable; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TimescaleTsKvCompositeKey implements Serializable { + + @Transient + private static final long serialVersionUID = -4089175869616037523L; + + private String tenantId; + private String entityId; + private String key; + private long ts; +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java new file mode 100644 index 0000000000..fa212dd2f9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java @@ -0,0 +1,184 @@ +/** + * 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.dao.model.sqlts.timescale; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.model.ToData; +import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity; + +import javax.persistence.Column; +import javax.persistence.ColumnResult; +import javax.persistence.ConstructorResult; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.NamedNativeQueries; +import javax.persistence.NamedNativeQuery; +import javax.persistence.SqlResultSetMapping; +import javax.persistence.SqlResultSetMappings; +import javax.persistence.Table; + +import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_AVG; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_AVG_QUERY; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_COUNT; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_COUNT_QUERY; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_MAX; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_MAX_QUERY; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_MIN; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_MIN_QUERY; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_SUM; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_SUM_QUERY; +import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FROM_WHERE_CLAUSE; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = "tenant_ts_kv") +@IdClass(TimescaleTsKvCompositeKey.class) +@SqlResultSetMappings({ + @SqlResultSetMapping( + name = "timescaleAggregationMapping", + classes = { + @ConstructorResult( + targetClass = TimescaleTsKvEntity.class, + columns = { + @ColumnResult(name = "tsBucket", type = Long.class), + @ColumnResult(name = "interval", type = Long.class), + @ColumnResult(name = "longValue", type = Long.class), + @ColumnResult(name = "doubleValue", type = Double.class), + @ColumnResult(name = "longCountValue", type = Long.class), + @ColumnResult(name = "doubleCountValue", type = Long.class), + @ColumnResult(name = "strValue", type = String.class), + @ColumnResult(name = "aggType", type = String.class), + } + ), + }), + @SqlResultSetMapping( + name = "timescaleCountMapping", + classes = { + @ConstructorResult( + targetClass = TimescaleTsKvEntity.class, + columns = { + @ColumnResult(name = "tsBucket", type = Long.class), + @ColumnResult(name = "interval", type = Long.class), + @ColumnResult(name = "booleanValueCount", type = Long.class), + @ColumnResult(name = "strValueCount", type = Long.class), + @ColumnResult(name = "longValueCount", type = Long.class), + @ColumnResult(name = "doubleValueCount", type = Long.class), + } + ) + }), +}) +@NamedNativeQueries({ + @NamedNativeQuery( + name = FIND_AVG, + query = FIND_AVG_QUERY + FROM_WHERE_CLAUSE, + resultSetMapping = "timescaleAggregationMapping" + ), + @NamedNativeQuery( + name = FIND_MAX, + query = FIND_MAX_QUERY + FROM_WHERE_CLAUSE, + resultSetMapping = "timescaleAggregationMapping" + ), + @NamedNativeQuery( + name = FIND_MIN, + query = FIND_MIN_QUERY + FROM_WHERE_CLAUSE, + resultSetMapping = "timescaleAggregationMapping" + ), + @NamedNativeQuery( + name = FIND_SUM, + query = FIND_SUM_QUERY + FROM_WHERE_CLAUSE, + resultSetMapping = "timescaleAggregationMapping" + ), + @NamedNativeQuery( + name = FIND_COUNT, + query = FIND_COUNT_QUERY + FROM_WHERE_CLAUSE, + resultSetMapping = "timescaleCountMapping" + ) +}) +public final class TimescaleTsKvEntity extends AbsractTsKvEntity implements ToData { + + @Id + @Column(name = TENANT_ID_COLUMN) + private String tenantId; + + public TimescaleTsKvEntity() { } + + public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType) { + if (!StringUtils.isEmpty(strValue)) { + this.strValue = strValue; + } + if (!isAllNull(tsBucket, interval, longValue, doubleValue, longCountValue, doubleCountValue)) { + this.ts = tsBucket + interval/2; + switch (aggType) { + case AVG: + double sum = 0.0; + if (longValue != null) { + sum += longValue; + } + if (doubleValue != null) { + sum += doubleValue; + } + long totalCount = longCountValue + doubleCountValue; + if (totalCount > 0) { + this.doubleValue = sum / (longCountValue + doubleCountValue); + } else { + this.doubleValue = 0.0; + } + break; + case SUM: + if (doubleCountValue > 0) { + this.doubleValue = doubleValue + (longValue != null ? longValue.doubleValue() : 0.0); + } else { + this.longValue = longValue; + } + break; + case MIN: + case MAX: + if (longCountValue > 0 && doubleCountValue > 0) { + this.doubleValue = MAX.equals(aggType) ? Math.max(doubleValue, longValue.doubleValue()) : Math.min(doubleValue, longValue.doubleValue()); + } else if (doubleCountValue > 0) { + this.doubleValue = doubleValue; + } else if (longCountValue > 0) { + this.longValue = longValue; + } + break; + } + } + } + + public TimescaleTsKvEntity(Long tsBucket, Long interval, Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount) { + if (!isAllNull(tsBucket, interval, booleanValueCount, strValueCount, longValueCount, doubleValueCount)) { + this.ts = tsBucket + interval/2; + if (booleanValueCount != 0) { + this.longValue = booleanValueCount; + } else if (strValueCount != 0) { + this.longValue = strValueCount; + } else { + this.longValue = longValueCount + doubleValueCount; + } + } + } + + @Override + public boolean isNotEmpty() { + return ts != null && (strValue != null || longValue != null || doubleValue != null || booleanValue != null); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvCompositeKey.java similarity index 95% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvCompositeKey.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvCompositeKey.java index 67bfe40370..0b7ae78e07 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvCompositeKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvCompositeKey.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sqlts.ts; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java similarity index 58% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java index 873f8e8c42..4440a3dd0c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java @@ -13,18 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sqlts.ts; import lombok.Data; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.model.ToData; +import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity; import javax.persistence.Column; import javax.persistence.Entity; @@ -34,25 +29,18 @@ import javax.persistence.Id; import javax.persistence.IdClass; import javax.persistence.Table; -import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; @Data @Entity @Table(name = "ts_kv") @IdClass(TsKvCompositeKey.class) -public final class TsKvEntity implements ToData { +public final class TsKvEntity extends AbsractTsKvEntity implements ToData { - private static final String SUM = "SUM"; - private static final String AVG = "AVG"; - private static final String MIN = "MIN"; - private static final String MAX = "MAX"; + @Id + @Enumerated(EnumType.STRING) + @Column(name = ENTITY_TYPE_COLUMN) + private EntityType entityType; public TsKvEntity() { } @@ -62,7 +50,7 @@ public final class TsKvEntity implements ToData { } public TsKvEntity(Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String aggType) { - if(!isAllNull(longValue, doubleValue, longCountValue, doubleCountValue)) { + if (!isAllNull(longValue, doubleValue, longCountValue, doubleCountValue)) { switch (aggType) { case AVG: double sum = 0.0; @@ -101,7 +89,7 @@ public final class TsKvEntity implements ToData { } public TsKvEntity(Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount) { - if(!isAllNull(booleanValueCount, strValueCount, longValueCount, doubleValueCount)) { + if (!isAllNull(booleanValueCount, strValueCount, longValueCount, doubleValueCount)) { if (booleanValueCount != 0) { this.longValue = booleanValueCount; } else if (strValueCount != 0) { @@ -112,60 +100,9 @@ public final class TsKvEntity implements ToData { } } - @Id - @Enumerated(EnumType.STRING) - @Column(name = ENTITY_TYPE_COLUMN) - private EntityType entityType; - - @Id - @Column(name = ENTITY_ID_COLUMN) - private String entityId; - - @Id - @Column(name = KEY_COLUMN) - private String key; - - @Id - @Column(name = TS_COLUMN) - private long ts; - - @Column(name = BOOLEAN_VALUE_COLUMN) - private Boolean booleanValue; - - @Column(name = STRING_VALUE_COLUMN) - private String strValue; - - @Column(name = LONG_VALUE_COLUMN) - private Long longValue; - - @Column(name = DOUBLE_VALUE_COLUMN) - private Double doubleValue; @Override - public TsKvEntry toData() { - KvEntry kvEntry = null; - if (strValue != null) { - kvEntry = new StringDataEntry(key, strValue); - } else if (longValue != null) { - kvEntry = new LongDataEntry(key, longValue); - } else if (doubleValue != null) { - kvEntry = new DoubleDataEntry(key, doubleValue); - } else if (booleanValue != null) { - kvEntry = new BooleanDataEntry(key, booleanValue); - } - return new BasicTsKvEntry(ts, kvEntry); - } - public boolean isNotEmpty() { return strValue != null || longValue != null || doubleValue != null || booleanValue != null; } - - private static boolean isAllNull(Object... args) { - for (Object arg : args) { - if(arg != null) { - return false; - } - } - return true; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestCompositeKey.java similarity index 95% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestCompositeKey.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestCompositeKey.java index 671fc0c662..004efa8b8b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestCompositeKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestCompositeKey.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sqlts.ts; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestEntity.java similarity index 98% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestEntity.java index 462abf1ed5..77a7d64fbb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestEntity.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sqlts.ts; import lombok.Data; import org.thingsboard.server.common.data.EntityType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 164d3036e1..75739c7048 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -26,7 +26,6 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -443,6 +442,7 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture> findByQuery(TenantId tenantId, EntityRelationsQuery query) { + //boolean fetchLastLevelOnly = true; log.trace("Executing findByQuery [{}]", query); RelationsSearchParameters params = query.getParameters(); final List filters = query.getFilters(); @@ -453,7 +453,7 @@ public class BaseRelationService implements RelationService { int maxLvl = params.getMaxLevel() > 0 ? params.getMaxLevel() : Integer.MAX_VALUE; try { - ListenableFuture> relationSet = findRelationsRecursively(tenantId, params.getEntityId(), params.getDirection(), params.getRelationTypeGroup(), maxLvl, new ConcurrentHashMap<>()); + ListenableFuture> relationSet = findRelationsRecursively(tenantId, params.getEntityId(), params.getDirection(), params.getRelationTypeGroup(), maxLvl, params.isFetchLastLevelOnly(), new ConcurrentHashMap<>()); return Futures.transform(relationSet, input -> { List relations = new ArrayList<>(); if (filters == null || filters.isEmpty()) { @@ -570,7 +570,7 @@ public class BaseRelationService implements RelationService { } private ListenableFuture> findRelationsRecursively(final TenantId tenantId, final EntityId rootId, final EntitySearchDirection direction, - RelationTypeGroup relationTypeGroup, int lvl, + RelationTypeGroup relationTypeGroup, int lvl, boolean fetchLastLevelOnly, final ConcurrentHashMap uniqueMap) throws Exception { if (lvl == 0) { return Futures.immediateFuture(Collections.emptySet()); @@ -596,10 +596,13 @@ public class BaseRelationService implements RelationService { } List>> futures = new ArrayList<>(); for (EntityId entityId : childrenIds) { - futures.add(findRelationsRecursively(tenantId, entityId, direction, relationTypeGroup, lvl, uniqueMap)); + futures.add(findRelationsRecursively(tenantId, entityId, direction, relationTypeGroup, lvl, fetchLastLevelOnly, uniqueMap)); } //TODO: try to remove this blocking operation List> relations = Futures.successfulAsList(futures).get(); + if (fetchLastLevelOnly && lvl > 0){ + children.clear(); + } relations.forEach(r -> r.forEach(children::add)); return Futures.immediateFuture(children); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index e016f2fcf7..5c21c5348b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -85,11 +85,5 @@ public abstract class DataValidator> { if (!expectedFields.containsAll(actualFields) || !actualFields.containsAll(expectedFields)) { throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!"); } - - for (String field : actualFields) { - if (!actualNode.get(field).isTextual()) { - throw new DataValidationException("Provided json structure can't contain non-text values '" + actualNode + "'!"); - } - } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index 3de38d1a7b..20d0c4a077 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -73,7 +73,9 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) { throw new DataValidationException("Changing key of admin settings entry is prohibited!"); } - validateJsonStructure(existentAdminSettings.getJsonValue(), adminSettings.getJsonValue()); + if (adminSettings.getKey().equals("mail")) { + validateJsonStructure(existentAdminSettings.getJsonValue(), adminSettings.getJsonValue()); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java new file mode 100644 index 0000000000..e50a60aed9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java @@ -0,0 +1,102 @@ +/** + * 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.dao.sql.attributes; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.stereotype.Repository; +import org.thingsboard.server.dao.model.sql.AttributeKvEntity; +import org.thingsboard.server.dao.util.SqlDao; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@SqlDao +@Repository +public abstract class AttributeKvInsertRepository { + + protected static final String BOOL_V = "bool_v"; + protected static final String STR_V = "str_v"; + protected static final String LONG_V = "long_v"; + protected static final String DBL_V = "dbl_v"; + + @PersistenceContext + protected EntityManager entityManager; + + public abstract void saveOrUpdate(AttributeKvEntity entity); + + protected void processSaveOrUpdate(AttributeKvEntity entity, String requestBoolValue, String requestStrValue, String requestLongValue, String requestDblValue) { + if (entity.getBooleanValue() != null) { + saveOrUpdateBoolean(entity, requestBoolValue); + } + if (entity.getStrValue() != null) { + saveOrUpdateString(entity, requestStrValue); + } + if (entity.getLongValue() != null) { + saveOrUpdateLong(entity, requestLongValue); + } + if (entity.getDoubleValue() != null) { + saveOrUpdateDouble(entity, requestDblValue); + } + } + + @Modifying + private void saveOrUpdateBoolean(AttributeKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getId().getEntityType().name()) + .setParameter("entity_id", entity.getId().getEntityId()) + .setParameter("attribute_type", entity.getId().getAttributeType()) + .setParameter("attribute_key", entity.getId().getAttributeKey()) + .setParameter("bool_v", entity.getBooleanValue()) + .setParameter("last_update_ts", entity.getLastUpdateTs()) + .executeUpdate(); + } + + @Modifying + private void saveOrUpdateString(AttributeKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getId().getEntityType().name()) + .setParameter("entity_id", entity.getId().getEntityId()) + .setParameter("attribute_type", entity.getId().getAttributeType()) + .setParameter("attribute_key", entity.getId().getAttributeKey()) + .setParameter("str_v", entity.getStrValue()) + .setParameter("last_update_ts", entity.getLastUpdateTs()) + .executeUpdate(); + } + + @Modifying + private void saveOrUpdateLong(AttributeKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getId().getEntityType().name()) + .setParameter("entity_id", entity.getId().getEntityId()) + .setParameter("attribute_type", entity.getId().getAttributeType()) + .setParameter("attribute_key", entity.getId().getAttributeKey()) + .setParameter("long_v", entity.getLongValue()) + .setParameter("last_update_ts", entity.getLastUpdateTs()) + .executeUpdate(); + } + + @Modifying + private void saveOrUpdateDouble(AttributeKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getId().getEntityType().name()) + .setParameter("entity_id", entity.getId().getEntityId()) + .setParameter("attribute_type", entity.getId().getAttributeType()) + .setParameter("attribute_key", entity.getId().getAttributeKey()) + .setParameter("dbl_v", entity.getDoubleValue()) + .setParameter("last_update_ts", entity.getLastUpdateTs()) + .executeUpdate(); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/HsqlAttributesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/HsqlAttributesInsertRepository.java new file mode 100644 index 0000000000..f2343cd7fe --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/HsqlAttributesInsertRepository.java @@ -0,0 +1,48 @@ +/** + * 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.dao.sql.attributes; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.AttributeKvEntity; +import org.thingsboard.server.dao.util.HsqlDao; +import org.thingsboard.server.dao.util.SqlDao; + +@SqlDao +@HsqlDao +@Repository +@Transactional +public class HsqlAttributesInsertRepository extends AttributeKvInsertRepository { + + private static final String ON_BOOL_VALUE_UPDATE_SET_NULLS = " attribute_kv.str_v = null, attribute_kv.long_v = null, attribute_kv.dbl_v = null "; + private static final String ON_STR_VALUE_UPDATE_SET_NULLS = " attribute_kv.bool_v = null, attribute_kv.long_v = null, attribute_kv.dbl_v = null "; + private static final String ON_LONG_VALUE_UPDATE_SET_NULLS = " attribute_kv.str_v = null, attribute_kv.bool_v = null, attribute_kv.dbl_v = null "; + private static final String ON_DBL_VALUE_UPDATE_SET_NULLS = " attribute_kv.str_v = null, attribute_kv.long_v = null, attribute_kv.bool_v = null "; + + private static final String INSERT_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_STR_STATEMENT = getInsertOrUpdateString(STR_V, ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_LONG_STATEMENT = getInsertOrUpdateString(LONG_V, ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS); + + @Override + public void saveOrUpdate(AttributeKvEntity entity) { + processSaveOrUpdate(entity, INSERT_BOOL_STATEMENT, INSERT_STR_STATEMENT, INSERT_LONG_STATEMENT, INSERT_DBL_STATEMENT); + } + + private static String getInsertOrUpdateString(String value, String nullValues) { + return "MERGE INTO attribute_kv USING(VALUES :entity_type, :entity_id, :attribute_type, :attribute_key, :" + value + ", :last_update_ts) A (entity_type, entity_id, attribute_type, attribute_key, " + value + ", last_update_ts) ON (attribute_kv.entity_type=A.entity_type AND attribute_kv.entity_id=A.entity_id AND attribute_kv.attribute_type=A.attribute_type AND attribute_kv.attribute_key=A.attribute_key) WHEN MATCHED THEN UPDATE SET attribute_kv." + value + " = A." + value + ", attribute_kv.last_update_ts = A.last_update_ts," + nullValues + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, attribute_type, attribute_key, " + value + ", last_update_ts) VALUES (A.entity_type, A.entity_id, A.attribute_type, A.attribute_key, A." + value + ", A.last_update_ts)"; + } +} \ No newline at end of file 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 dc65018cf1..b8ad6f8271 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 @@ -47,6 +47,9 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Autowired private AttributeKvRepository attributeKvRepository; + @Autowired + private AttributeKvInsertRepository attributeKvInsertRepository; + @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey) { AttributeKvCompositeKey compositeKey = @@ -87,11 +90,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl entity.setLongValue(attribute.getLongValue().orElse(null)); entity.setBooleanValue(attribute.getBooleanValue().orElse(null)); return service.submit(() -> { - attributeKvRepository.save(entity); + attributeKvInsertRepository.saveOrUpdate(entity); return null; }); } + @Override public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys) { List entitiesToDelete = keys diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/PsqlAttributesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/PsqlAttributesInsertRepository.java new file mode 100644 index 0000000000..23615d6036 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/PsqlAttributesInsertRepository.java @@ -0,0 +1,48 @@ +/** + * 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.dao.sql.attributes; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.AttributeKvEntity; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.SqlDao; + +@SqlDao +@PsqlDao +@Repository +@Transactional +public class PsqlAttributesInsertRepository extends AttributeKvInsertRepository { + + private static final String ON_BOOL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, dbl_v = null"; + private static final String ON_STR_VALUE_UPDATE_SET_NULLS = "bool_v = null, long_v = null, dbl_v = null"; + private static final String ON_LONG_VALUE_UPDATE_SET_NULLS = "str_v = null, bool_v = null, dbl_v = null"; + private static final String ON_DBL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, bool_v = null"; + + private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V , ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS); + + @Override + public void saveOrUpdate(AttributeKvEntity entity) { + processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT); + } + + private static String getInsertOrUpdateString(String value, String nullValues) { + return "INSERT INTO attribute_kv (entity_type, entity_id, attribute_type, attribute_key, " + value + ", last_update_ts) VALUES (:entity_type, :entity_id, :attribute_type, :attribute_key, :" + value + ", :last_update_ts) ON CONFLICT (entity_type, entity_id, attribute_type, attribute_key) DO UPDATE SET " + value + " = :" + value + ", last_update_ts = :last_update_ts," + nullValues; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java index d6f5498acc..a2c18c541b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java @@ -26,6 +26,7 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -101,34 +102,34 @@ public class JpaAuditLogDao extends JpaAbstractDao imp } @Override - public List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { - return findAuditLogs(tenantId, entityId, null, null, pageLink); + public List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { + return findAuditLogs(tenantId, entityId, null, null, actionTypes, pageLink); } @Override - public List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { - return findAuditLogs(tenantId, null, customerId, null, pageLink); + public List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { + return findAuditLogs(tenantId, null, customerId, null, actionTypes, pageLink); } @Override - public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { - return findAuditLogs(tenantId, null, null, userId, pageLink); + public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { + return findAuditLogs(tenantId, null, null, userId, actionTypes, pageLink); } @Override - public List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { - return findAuditLogs(tenantId, null, null, null, pageLink); + public List findAuditLogsByTenantId(UUID tenantId, List actionTypes, TimePageLink pageLink) { + return findAuditLogs(tenantId, null, null, null, actionTypes, pageLink); } - private List findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, TimePageLink pageLink) { + private List findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, List actionTypes, TimePageLink pageLink) { Specification timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id"); - Specification fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId); + Specification fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId, actionTypes); Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC; Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY); return DaoUtil.convertDataList(auditLogRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent()); } - private Specification getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId) { + private Specification getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, List actionTypes) { return (root, criteriaQuery, criteriaBuilder) -> { List predicates = new ArrayList<>(); if (tenantId != null) { @@ -142,12 +143,15 @@ public class JpaAuditLogDao extends JpaAbstractDao imp predicates.add(entityIdPredicate); } if (customerId != null) { - Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId())); - predicates.add(tenantIdPredicate); + Predicate customerIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId())); + predicates.add(customerIdPredicate); } if (userId != null) { - Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId())); - predicates.add(tenantIdPredicate); + Predicate userIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId())); + predicates.add(userIdPredicate); + } + if (actionTypes != null && !actionTypes.isEmpty()) { + predicates.add(root.get("actionType").in(actionTypes)); } return criteriaBuilder.and(predicates.toArray(new Predicate[]{})); }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java new file mode 100644 index 0000000000..051453cf63 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java @@ -0,0 +1,96 @@ +/** + * 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.dao.sql.event; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.dao.model.sql.EventEntity; +import org.thingsboard.server.dao.util.SqlDao; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Slf4j +@SqlDao +@Repository +public abstract class EventInsertRepository { + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + protected PlatformTransactionManager transactionManager; + + public abstract EventEntity saveOrUpdate(EventEntity entity); + + protected EventEntity saveAndGet(EventEntity entity, String insertOrUpdateOnPrimaryKeyConflict, String insertOrUpdateOnUniqueKeyConflict) { + EventEntity eventEntity = null; + TransactionStatus insertTransaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRED); + try { + eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnPrimaryKeyConflict); + transactionManager.commit(insertTransaction); + } catch (Throwable throwable) { + transactionManager.rollback(insertTransaction); + if (throwable.getCause() instanceof ConstraintViolationException) { + log.trace("Insert request leaded in a violation of a defined integrity constraint {} for Entity with entityId {} and entityType {}", throwable.getMessage(), entity.getEventUid(), entity.getEventType()); + TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + try { + eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); + } catch (Throwable th) { + log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); + transactionManager.rollback(transaction); + } + transactionManager.commit(transaction); + } else { + log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); + } + } + return eventEntity; + } + + @Modifying + protected abstract EventEntity doProcessSaveOrUpdate(EventEntity entity, String query); + + protected Query getQuery(EventEntity entity, String query) { + return entityManager.createNativeQuery(query, EventEntity.class) + .setParameter("id", UUIDConverter.fromTimeUUID(entity.getId())) + .setParameter("body", entity.getBody().toString()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("entity_type", entity.getEntityType().name()) + .setParameter("event_type", entity.getEventType()) + .setParameter("event_uid", entity.getEventUid()) + .setParameter("tenant_id", entity.getTenantId()); + } + + private EventEntity processSaveOrUpdate(EventEntity entity, String query) { + return doProcessSaveOrUpdate(entity, query); + } + + private TransactionStatus getTransactionStatus(int propagationRequired) { + DefaultTransactionDefinition insertDefinition = new DefaultTransactionDefinition(); + insertDefinition.setPropagationBehavior(propagationRequired); + return transactionManager.getTransaction(insertDefinition); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java new file mode 100644 index 0000000000..e34cc7b70c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java @@ -0,0 +1,50 @@ +/** + * 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.dao.sql.event; + +import org.springframework.stereotype.Repository; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.dao.model.sql.EventEntity; +import org.thingsboard.server.dao.util.HsqlDao; +import org.thingsboard.server.dao.util.SqlDao; + +@SqlDao +@HsqlDao +@Repository +public class HsqlEventInsertRepository extends EventInsertRepository { + + private static final String P_KEY_CONFLICT_STATEMENT = "(event.id=I.id)"; + private static final String UNQ_KEY_CONFLICT_STATEMENT = "(event.tenant_id=I.tenant_id AND event.entity_type=I.entity_type AND event.entity_id=I.entity_id AND event.event_type=I.event_type AND event.event_uid=I.event_uid)"; + + private static final String INSERT_OR_UPDATE_ON_P_KEY_CONFLICT = getInsertString(P_KEY_CONFLICT_STATEMENT); + private static final String INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT = getInsertString(UNQ_KEY_CONFLICT_STATEMENT); + + @Override + public EventEntity saveOrUpdate(EventEntity entity) { + return saveAndGet(entity, INSERT_OR_UPDATE_ON_P_KEY_CONFLICT, INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT); + } + + @Override + protected EventEntity doProcessSaveOrUpdate(EventEntity entity, String query) { + getQuery(entity, query).executeUpdate(); + return entityManager.find(EventEntity.class, UUIDConverter.fromTimeUUID(entity.getId())); + } + + private static String getInsertString(String conflictStatement) { + return "MERGE INTO event USING (VALUES :id, :body, :entity_id, :entity_type, :event_type, :event_uid, :tenant_id) I (id, body, entity_id, entity_type, event_type, event_uid, tenant_id) ON " + conflictStatement + " WHEN MATCHED THEN UPDATE SET event.id = I.id, event.body = I.body, event.entity_id = I.entity_id, event.entity_type = I.entity_type, event.event_type = I.event_type, event.event_uid = I.event_uid, event.tenant_id = I.tenant_id" + + " WHEN NOT MATCHED THEN INSERT (id, body, entity_id, entity_type, event_type, event_uid, tenant_id) VALUES (I.id, I.body, I.entity_id, I.entity_type, I.event_type, I.event_uid, I.tenant_id)"; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index c33cb6bf34..b75e231b64 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -61,6 +61,9 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao getEntityClass() { return EventEntity.class; @@ -147,7 +150,7 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao getEntityFieldsSpec(UUID tenantId, EntityId entityId, String eventType) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java new file mode 100644 index 0000000000..e4fd1ed37c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java @@ -0,0 +1,53 @@ +/** + * 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.dao.sql.event; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; +import org.thingsboard.server.dao.model.sql.EventEntity; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.SqlDao; + +@Slf4j +@SqlDao +@PsqlDao +@Repository +public class PsqlEventInsertRepository extends EventInsertRepository { + + private static final String P_KEY_CONFLICT_STATEMENT = "(id)"; + private static final String UNQ_KEY_CONFLICT_STATEMENT = "(tenant_id, entity_type, entity_id, event_type, event_uid)"; + + private static final String UPDATE_P_KEY_STATEMENT = "id = :id"; + private static final String UPDATE_UNQ_KEY_STATEMENT = "tenant_id = :tenant_id, entity_type = :entity_type, entity_id = :entity_id, event_type = :event_type, event_uid = :event_uid"; + + private static final String INSERT_OR_UPDATE_ON_P_KEY_CONFLICT = getInsertOrUpdateString(P_KEY_CONFLICT_STATEMENT, UPDATE_UNQ_KEY_STATEMENT); + private static final String INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT = getInsertOrUpdateString(UNQ_KEY_CONFLICT_STATEMENT, UPDATE_P_KEY_STATEMENT); + + @Override + public EventEntity saveOrUpdate(EventEntity entity) { + return saveAndGet(entity, INSERT_OR_UPDATE_ON_P_KEY_CONFLICT, INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT); + } + + @Override + protected EventEntity doProcessSaveOrUpdate(EventEntity entity, String query) { + return (EventEntity) getQuery(entity, query).getSingleResult(); + + } + + private static String getInsertOrUpdateString(String eventKeyStatement, String updateKeyStatement) { + return "INSERT INTO event (id, body, entity_id, entity_type, event_type, event_uid, tenant_id) VALUES (:id, :body, :entity_id, :entity_type, :event_type, :event_uid, :tenant_id) ON CONFLICT " + eventKeyStatement + " DO UPDATE SET body = :body, " + updateKeyStatement + " returning *"; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java new file mode 100644 index 0000000000..cf3596a864 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -0,0 +1,130 @@ +/** + * 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.dao.sqlts; + +import com.google.common.base.Function; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; +import org.thingsboard.server.dao.timeseries.TsInsertExecutorType; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningExecutorService { + + private static final String DESC_ORDER = "DESC"; + + @Value("${sql.ts_inserts_executor_type}") + private String insertExecutorType; + + @Value("${sql.ts_inserts_fixed_thread_pool_size}") + private int insertFixedThreadPoolSize; + + @Value("${spring.datasource.hikari.maximumPoolSize}") + private int maximumPoolSize; + + protected ListeningExecutorService insertService; + + @PostConstruct + void init() { + Optional executorTypeOptional = TsInsertExecutorType.parse(insertExecutorType); + TsInsertExecutorType executorType; + executorType = executorTypeOptional.orElse(TsInsertExecutorType.FIXED); + switch (executorType) { + case SINGLE: + insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); + break; + case FIXED: + case CACHED: + int poolSize = insertFixedThreadPoolSize; + if (poolSize <= 0) { + poolSize = maximumPoolSize * 4; + } + insertService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(poolSize)); + break; + } + } + + @PreDestroy + void preDestroy() { + if (insertService != null) { + insertService.shutdown(); + } + } + + protected ListenableFuture> processFindAllAsync(TenantId tenantId, EntityId entityId, List queries) { + List>> futures = queries + .stream() + .map(query -> findAllAsync(tenantId, entityId, query)) + .collect(Collectors.toList()); + return Futures.transform(Futures.allAsList(futures), new Function>, List>() { + @Nullable + @Override + public List apply(@Nullable List> results) { + if (results == null || results.isEmpty()) { + return null; + } + return results.stream() + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + }, service); + } + + protected abstract ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query); + + protected ListenableFuture> getTskvEntriesFuture(ListenableFuture>> future) { + return Futures.transform(future, new Function>, List>() { + @Nullable + @Override + public List apply(@Nullable List> results) { + if (results == null || results.isEmpty()) { + return null; + } + return results.stream() + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + }, service); + } + + protected ListenableFuture> findNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + long startTs = 0; + long endTs = query.getStartTs() - 1; + ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, + Aggregation.NONE, DESC_ORDER); + return findAllAsync(tenantId, entityId, findNewLatestQuery); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java new file mode 100644 index 0000000000..608933c3f7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java @@ -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. + */ +package org.thingsboard.server.dao.sqlts; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.stereotype.Repository; +import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@Repository +public abstract class AbstractTimeseriesInsertRepository { + + protected static final String BOOL_V = "bool_v"; + protected static final String STR_V = "str_v"; + protected static final String LONG_V = "long_v"; + protected static final String DBL_V = "dbl_v"; + + @PersistenceContext + protected EntityManager entityManager; + + public abstract void saveOrUpdate(T entity); + + protected void processSaveOrUpdate(T entity, String requestBoolValue, String requestStrValue, String requestLongValue, String requestDblValue) { + if (entity.getBooleanValue() != null) { + saveOrUpdateBoolean(entity, requestBoolValue); + } + if (entity.getStrValue() != null) { + saveOrUpdateString(entity, requestStrValue); + } + if (entity.getLongValue() != null) { + saveOrUpdateLong(entity, requestLongValue); + } + if (entity.getDoubleValue() != null) { + saveOrUpdateDouble(entity, requestDblValue); + } + } + + @Modifying + protected abstract void saveOrUpdateBoolean(T entity, String query); + + @Modifying + protected abstract void saveOrUpdateString(T entity, String query); + + @Modifying + protected abstract void saveOrUpdateLong(T entity, String query); + + @Modifying + protected abstract void saveOrUpdateDouble(T entity, String query); + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java new file mode 100644 index 0000000000..ab2cf1fff5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java @@ -0,0 +1,101 @@ +/** + * 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.dao.sqlts.timescale; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Repository; +import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvEntity; +import org.thingsboard.server.dao.util.TimescaleDBTsDao; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Repository +@TimescaleDBTsDao +public class AggregationRepository { + + public static final String FIND_AVG = "findAvg"; + public static final String FIND_MAX = "findMax"; + public static final String FIND_MIN = "findMin"; + public static final String FIND_SUM = "findSum"; + public static final String FIND_COUNT = "findCount"; + + + public static final String FROM_WHERE_CLAUSE = "FROM tenant_ts_kv tskv WHERE tskv.tenant_id = cast(:tenantId AS varchar) AND tskv.entity_id = cast(:entityId AS varchar) AND tskv.key= cast(:entityKey AS varchar) AND tskv.ts > :startTs AND tskv.ts <= :endTs GROUP BY tskv.tenant_id, tskv.entity_id, tskv.key, tsBucket ORDER BY tskv.tenant_id, tskv.entity_id, tskv.key, tsBucket"; + + public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType "; + + public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType "; + + public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType "; + + public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'SUM' AS aggType "; + + public static final String FIND_COUNT_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount "; + + @PersistenceContext + private EntityManager entityManager; + + @Async + public CompletableFuture> findAvg(String tenantId, String entityId, String entityKey, long timeBucket, long startTs, long endTs) { + @SuppressWarnings("unchecked") + List resultList = getResultList(tenantId, entityId, entityKey, timeBucket, startTs, endTs, FIND_AVG); + return CompletableFuture.supplyAsync(() -> resultList); + } + + @Async + public CompletableFuture> findMax(String tenantId, String entityId, String entityKey, long timeBucket, long startTs, long endTs) { + @SuppressWarnings("unchecked") + List resultList = getResultList(tenantId, entityId, entityKey, timeBucket, startTs, endTs, FIND_MAX); + return CompletableFuture.supplyAsync(() -> resultList); + } + + @Async + public CompletableFuture> findMin(String tenantId, String entityId, String entityKey, long timeBucket, long startTs, long endTs) { + @SuppressWarnings("unchecked") + List resultList = getResultList(tenantId, entityId, entityKey, timeBucket, startTs, endTs, FIND_MIN); + return CompletableFuture.supplyAsync(() -> resultList); + } + + @Async + public CompletableFuture> findSum(String tenantId, String entityId, String entityKey, long timeBucket, long startTs, long endTs) { + @SuppressWarnings("unchecked") + List resultList = getResultList(tenantId, entityId, entityKey, timeBucket, startTs, endTs, FIND_SUM); + return CompletableFuture.supplyAsync(() -> resultList); + } + + @Async + public CompletableFuture> findCount(String tenantId, String entityId, String entityKey, long timeBucket, long startTs, long endTs) { + @SuppressWarnings("unchecked") + List resultList = getResultList(tenantId, entityId, entityKey, timeBucket, startTs, endTs, FIND_COUNT); + return CompletableFuture.supplyAsync(() -> resultList); + } + + private List getResultList(String tenantId, String entityId, String entityKey, long timeBucket, long startTs, long endTs, String query) { + return entityManager.createNamedQuery(query) + .setParameter("tenantId", tenantId) + .setParameter("entityId", entityId) + .setParameter("entityKey", entityKey) + .setParameter("timeBucket", timeBucket) + .setParameter("startTs", startTs) + .setParameter("endTs", endTs) + .getResultList(); + } + + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleInsertRepository.java new file mode 100644 index 0000000000..3c87e9f909 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleInsertRepository.java @@ -0,0 +1,93 @@ +/** + * 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.dao.sqlts.timescale; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvEntity; +import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.TimescaleDBTsDao; + +@TimescaleDBTsDao +@PsqlDao +@Repository +@Transactional +public class TimescaleInsertRepository extends AbstractTimeseriesInsertRepository { + + private static final String ON_BOOL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, dbl_v = null"; + private static final String ON_STR_VALUE_UPDATE_SET_NULLS = "bool_v = null, long_v = null, dbl_v = null"; + private static final String ON_LONG_VALUE_UPDATE_SET_NULLS = "str_v = null, bool_v = null, dbl_v = null"; + private static final String ON_DBL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, bool_v = null"; + + private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V , ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS); + + @Override + public void saveOrUpdate(TimescaleTsKvEntity entity) { + processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT); + } + + @Override + protected void saveOrUpdateBoolean(TimescaleTsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("tenant_id", entity.getTenantId()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("bool_v", entity.getBooleanValue()) + .executeUpdate(); + } + + @Override + protected void saveOrUpdateString(TimescaleTsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("tenant_id", entity.getTenantId()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("str_v", entity.getStrValue()) + .executeUpdate(); + } + + @Override + protected void saveOrUpdateLong(TimescaleTsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("tenant_id", entity.getTenantId()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("long_v", entity.getLongValue()) + .executeUpdate(); + } + + @Override + protected void saveOrUpdateDouble(TimescaleTsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("tenant_id", entity.getTenantId()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("dbl_v", entity.getDoubleValue()) + .executeUpdate(); + } + + private static String getInsertOrUpdateString(String value, String nullValues) { + return "INSERT INTO tenant_ts_kv(tenant_id, entity_id, key, ts, " + value + ") VALUES (:tenant_id, :entity_id, :key, :ts, :" + value + ") ON CONFLICT (tenant_id, entity_id, key, ts) DO UPDATE SET " + value + " = :" + value + ", ts = :ts," + nullValues; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java new file mode 100644 index 0000000000..844f22a31c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -0,0 +1,295 @@ +/** + * 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.dao.sqlts.timescale; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvQuery; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvEntity; +import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao; +import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; +import org.thingsboard.server.dao.timeseries.TimeseriesDao; +import org.thingsboard.server.dao.util.TimescaleDBTsDao; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; + + +@Component +@Slf4j +@TimescaleDBTsDao +public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements TimeseriesDao { + + private static final String TS = "ts"; + + @Autowired + private TsKvTimescaleRepository tsKvRepository; + + @Autowired + private AggregationRepository aggregationRepository; + + @Autowired + private AbstractTimeseriesInsertRepository insertRepository; + + @Override + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { + return processFindAllAsync(tenantId, entityId, queries); + } + + protected ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + if (query.getAggregation() == Aggregation.NONE) { + return findAllAsyncWithLimit(tenantId, entityId, query); + } else { + long startTs = query.getStartTs(); + long endTs = query.getEndTs(); + long timeBucket = query.getInterval(); + ListenableFuture>> future = findAndAggregateAsync(tenantId, entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation()); + return getTskvEntriesFuture(future); + } + } + + private ListenableFuture> findAllAsyncWithLimit(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + return Futures.immediateFuture( + DaoUtil.convertDataList( + tsKvRepository.findAllWithLimit( + fromTimeUUID(tenantId.getId()), + fromTimeUUID(entityId.getId()), + query.getKey(), + query.getStartTs(), + query.getEndTs(), + new PageRequest(0, query.getLimit(), + new Sort(Sort.Direction.fromString( + query.getOrderBy()), "ts"))))); + } + + + @Override + public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { + ListenableFuture> future = getLatest(tenantId, entityId, key, 0L, System.currentTimeMillis()); + return Futures.transform(future, latest -> { + if (!CollectionUtils.isEmpty(latest)) { + return DaoUtil.getData(latest.get(0)); + } else { + return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); + } + }, service); + } + + @Override + public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { + return Futures.immediateFuture(DaoUtil.convertDataList(Lists.newArrayList(tsKvRepository.findAllLatestValues(fromTimeUUID(tenantId.getId()), fromTimeUUID(entityId.getId()))))); + } + + @Override + public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + TimescaleTsKvEntity entity = new TimescaleTsKvEntity(); + entity.setTenantId(fromTimeUUID(tenantId.getId())); + entity.setEntityId(fromTimeUUID(entityId.getId())); + entity.setTs(tsKvEntry.getTs()); + entity.setKey(tsKvEntry.getKey()); + entity.setStrValue(tsKvEntry.getStrValue().orElse(null)); + entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); + entity.setLongValue(tsKvEntry.getLongValue().orElse(null)); + entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); + log.trace("Saving entity to timescale db: {}", entity); + return insertService.submit(() -> { + insertRepository.saveOrUpdate(entity); + return null; + }); + } + + @Override + public ListenableFuture savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { + return insertService.submit(() -> null); + } + + @Override + public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + return insertService.submit(() -> null); + } + + @Override + public ListenableFuture remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + return service.submit(() -> { + tsKvRepository.delete( + fromTimeUUID(tenantId.getId()), + fromTimeUUID(entityId.getId()), + query.getKey(), + query.getStartTs(), + query.getEndTs()); + return null; + }); + } + + @Override + public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + return service.submit(() -> null); + } + + @Override + public ListenableFuture removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + return service.submit(() -> null); + } + + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); + return Futures.transformAsync(future, entryList -> { + if (entryList.size() == 1) { + return save(tenantId, entityId, entryList.get(0), 0L); + } else { + log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); + } + return Futures.immediateFuture(null); + }, service); + } + + private ListenableFuture> findLatestByQuery(TenantId tenantId, EntityId entityId, TsKvQuery query) { + return getLatest(tenantId, entityId, query.getKey(), query.getStartTs(), query.getEndTs()); + } + + private ListenableFuture> getLatest(TenantId tenantId, EntityId entityId, String key, long start, long end) { + return Futures.immediateFuture(tsKvRepository.findAllWithLimit( + fromTimeUUID(tenantId.getId()), + fromTimeUUID(entityId.getId()), + key, + start, + end, + new PageRequest(0, 1, + new Sort(Sort.Direction.DESC, TS)))); + } + + private ListenableFuture>> findAndAggregateAsync(TenantId tenantId, EntityId entityId, String key, long startTs, long endTs, long timeBucket, Aggregation aggregation) { + String entityIdStr = fromTimeUUID(entityId.getId()); + String tenantIdStr = fromTimeUUID(tenantId.getId()); + CompletableFuture> listCompletableFuture = switchAgregation(key, startTs, endTs, timeBucket, aggregation, entityIdStr, tenantIdStr); + SettableFuture> listenableFuture = SettableFuture.create(); + listCompletableFuture.whenComplete((timescaleTsKvEntities, throwable) -> { + if (throwable != null) { + listenableFuture.setException(throwable); + } else { + listenableFuture.set(timescaleTsKvEntities); + } + }); + return Futures.transform(listenableFuture, timescaleTsKvEntities -> { + if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) { + List> result = new ArrayList<>(); + timescaleTsKvEntities.forEach(entity -> { + if(entity != null && entity.isNotEmpty()) { + entity.setEntityId(entityIdStr); + entity.setTenantId(tenantIdStr); + entity.setKey(key); + result.add(Optional.of(DaoUtil.getData(entity))); + } else { + result.add(Optional.empty()); + } + }); + return result; + } else { + return Collections.emptyList(); + } + }); + } + + private CompletableFuture> switchAgregation(String key, long startTs, long endTs, long timeBucket, Aggregation aggregation, String entityIdStr, String tenantIdStr) { + switch (aggregation) { + case AVG: + return findAvg(key, startTs, endTs, timeBucket, entityIdStr, tenantIdStr); + case MAX: + return findMax(key, startTs, endTs, timeBucket, entityIdStr, tenantIdStr); + case MIN: + return findMin(key, startTs, endTs, timeBucket, entityIdStr, tenantIdStr); + case SUM: + return findSum(key, startTs, endTs, timeBucket, entityIdStr, tenantIdStr); + case COUNT: + return findCount(key, startTs, endTs, timeBucket, entityIdStr, tenantIdStr); + default: + throw new IllegalArgumentException("Not supported aggregation type: " + aggregation); + } + } + + private CompletableFuture> findAvg(String key, long startTs, long endTs, long timeBucket, String entityIdStr, String tenantIdStr) { + return aggregationRepository.findAvg( + tenantIdStr, + entityIdStr, + key, + timeBucket, + startTs, + endTs); + } + + private CompletableFuture> findMax(String key, long startTs, long endTs, long timeBucket, String entityIdStr, String tenantIdStr) { + return aggregationRepository.findMax( + tenantIdStr, + entityIdStr, + key, + timeBucket, + startTs, + endTs); + } + + private CompletableFuture> findMin(String key, long startTs, long endTs, long timeBucket, String entityIdStr, String tenantIdStr) { + return aggregationRepository.findMin( + tenantIdStr, + entityIdStr, + key, + timeBucket, + startTs, + endTs); + + } + + private CompletableFuture> findSum(String key, long startTs, long endTs, long timeBucket, String entityIdStr, String tenantIdStr) { + return aggregationRepository.findSum( + tenantIdStr, + entityIdStr, + key, + timeBucket, + startTs, + endTs); + } + + private CompletableFuture> findCount(String key, long startTs, long endTs, long timeBucket, String entityIdStr, String tenantIdStr) { + return aggregationRepository.findCount( + tenantIdStr, + entityIdStr, + key, + timeBucket, + startTs, + endTs); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java new file mode 100644 index 0000000000..af15e8546e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java @@ -0,0 +1,66 @@ +/** + * 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.dao.sqlts.timescale; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvCompositeKey; +import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvEntity; +import org.thingsboard.server.dao.util.TimescaleDBTsDao; + +import java.util.List; + +@TimescaleDBTsDao +public interface TsKvTimescaleRepository extends CrudRepository { + + @Query("SELECT tskv FROM TimescaleTsKvEntity tskv WHERE tskv.tenantId = :tenantId " + + "AND tskv.entityId = :entityId " + + "AND tskv.key = :entityKey " + + "AND tskv.ts > :startTs AND tskv.ts <= :endTs") + List findAllWithLimit( + @Param("tenantId") String tenantId, + @Param("entityId") String entityId, + @Param("entityKey") String key, + @Param("startTs") long startTs, + @Param("endTs") long endTs, Pageable pageable); + + @Query(value = "SELECT tskv.tenant_id as tenant_id, tskv.entity_id as entity_id, tskv.key as key, last(tskv.ts,tskv.ts) as ts," + + " last(tskv.bool_v, tskv.ts) as bool_v, last(tskv.str_v, tskv.ts) as str_v," + + " last(tskv.long_v, tskv.ts) as long_v, last(tskv.dbl_v, tskv.ts) as dbl_v" + + " FROM tenant_ts_kv tskv WHERE tskv.tenant_id = cast(:tenantId AS varchar) " + + "AND tskv.entity_id = cast(:entityId AS varchar) " + + "GROUP BY tskv.tenant_id, tskv.entity_id, tskv.key", nativeQuery = true) + List findAllLatestValues( + @Param("tenantId") String tenantId, + @Param("entityId") String entityId); + + @Transactional + @Modifying + @Query("DELETE FROM TimescaleTsKvEntity tskv WHERE tskv.tenantId = :tenantId " + + "AND tskv.entityId = :entityId " + + "AND tskv.key = :entityKey " + + "AND tskv.ts > :startTs AND tskv.ts <= :endTs") + void delete(@Param("tenantId") String tenantId, + @Param("entityId") String entityId, + @Param("entityKey") String key, + @Param("startTs") long startTs, + @Param("endTs") long endTs); + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java new file mode 100644 index 0000000000..3ac5cc67a9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java @@ -0,0 +1,93 @@ +/** + * 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.dao.sqlts.ts; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; +import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; +import org.thingsboard.server.dao.util.HsqlDao; +import org.thingsboard.server.dao.util.SqlTsDao; + +@SqlTsDao +@HsqlDao +@Repository +@Transactional +public class HsqlTimeseriesInsertRepository extends AbstractTimeseriesInsertRepository { + + private static final String ON_BOOL_VALUE_UPDATE_SET_NULLS = " ts_kv.str_v = null, ts_kv.long_v = null, ts_kv.dbl_v = null "; + private static final String ON_STR_VALUE_UPDATE_SET_NULLS = " ts_kv.bool_v = null, ts_kv.long_v = null, ts_kv.dbl_v = null "; + private static final String ON_LONG_VALUE_UPDATE_SET_NULLS = " ts_kv.str_v = null, ts_kv.bool_v = null, ts_kv.dbl_v = null "; + private static final String ON_DBL_VALUE_UPDATE_SET_NULLS = " ts_kv.str_v = null, ts_kv.long_v = null, ts_kv.bool_v = null "; + + private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V , ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS); + + private static String getInsertOrUpdateString(String value, String nullValues) { + return "MERGE INTO ts_kv USING(VALUES :entity_type, :entity_id, :key, :ts, :" + value + ") A (entity_type, entity_id, key, ts, " + value + ") ON (ts_kv.entity_type=A.entity_type AND ts_kv.entity_id=A.entity_id AND ts_kv.key=A.key AND ts_kv.ts=A.ts) WHEN MATCHED THEN UPDATE SET ts_kv." + value + " = A." + value + ", ts_kv.ts = A.ts," + nullValues + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, key, ts, " + value + ") VALUES (A.entity_type, A.entity_id, A.key, A.ts, A." + value + ")"; + } + + @Override + public void saveOrUpdate(TsKvEntity entity) { + processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT); + } + + @Override + protected void saveOrUpdateBoolean(TsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getEntityType().name()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("bool_v", entity.getBooleanValue()) + .executeUpdate(); + } + + @Override + protected void saveOrUpdateString(TsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getEntityType().name()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("str_v", entity.getStrValue()) + .executeUpdate(); + } + + @Override + protected void saveOrUpdateLong(TsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getEntityType().name()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("long_v", entity.getLongValue()) + .executeUpdate(); + } + + @Override + protected void saveOrUpdateDouble(TsKvEntity entity, String query) { + entityManager.createNativeQuery(query) + .setParameter("entity_type", entity.getEntityType().name()) + .setParameter("entity_id", entity.getEntityId()) + .setParameter("key", entity.getKey()) + .setParameter("ts", entity.getTs()) + .setParameter("dbl_v", entity.getDoubleValue()) + .executeUpdate(); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java similarity index 62% rename from dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java index 2b36eb80b9..9e4e281ebd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java @@ -13,19 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.timeseries; +package org.thingsboard.server.dao.sqlts.ts; -import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @@ -33,31 +29,27 @@ import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.sql.TsKvEntity; -import org.thingsboard.server.dao.model.sql.TsKvLatestCompositeKey; -import org.thingsboard.server.dao.model.sql.TsKvLatestEntity; -import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestCompositeKey; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestEntity; +import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao; +import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; import org.thingsboard.server.dao.timeseries.SimpleListenableFuture; import org.thingsboard.server.dao.timeseries.TimeseriesDao; -import org.thingsboard.server.dao.timeseries.TsInsertExecutorType; import org.thingsboard.server.dao.util.SqlTsDao; import javax.annotation.Nullable; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; @@ -66,17 +58,7 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; @Component @Slf4j @SqlTsDao -public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao { - - private static final String DESC_ORDER = "DESC"; - - @Value("${sql.ts_inserts_executor_type}") - private String insertExecutorType; - - @Value("${sql.ts_inserts_fixed_thread_pool_size}") - private int insertFixedThreadPoolSize; - - private ListeningExecutorService insertService; +public class JpaTimeseriesDao extends AbstractSqlTimeseriesDao implements TimeseriesDao { @Autowired private TsKvRepository tsKvRepository; @@ -84,53 +66,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp @Autowired private TsKvLatestRepository tsKvLatestRepository; - @PostConstruct - public void init() { - Optional executorTypeOptional = TsInsertExecutorType.parse(insertExecutorType); - TsInsertExecutorType executorType; - if (executorTypeOptional.isPresent()) { - executorType = executorTypeOptional.get(); - } else { - executorType = TsInsertExecutorType.FIXED; - } - switch (executorType) { - case SINGLE: - insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); - break; - case FIXED: - int poolSize = insertFixedThreadPoolSize; - if (poolSize <= 0) { - poolSize = 10; - } - insertService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(poolSize)); - break; - case CACHED: - insertService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); - break; - } - } + @Autowired + private AbstractTimeseriesInsertRepository insertRepository; @Override public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { - List>> futures = queries - .stream() - .map(query -> findAllAsync(tenantId, entityId, query)) - .collect(Collectors.toList()); - return Futures.transform(Futures.allAsList(futures), new Function>, List>() { - @Nullable - @Override - public List apply(@Nullable List> results) { - if (results == null || results.isEmpty()) { - return null; - } - return results.stream() - .flatMap(List::stream) - .collect(Collectors.toList()); - } - }, service); + return processFindAllAsync(tenantId, entityId, queries); } - private ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + protected ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { return findAllAsyncWithLimit(entityId, query); } else { @@ -140,98 +84,25 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp long startTs = stepTs; long endTs = stepTs + query.getInterval(); long ts = startTs + (endTs - startTs) / 2; - futures.add(findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation())); + futures.add(findAndAggregateAsync(tenantId, entityId, query.getKey(), startTs, endTs, ts, query.getAggregation())); stepTs = endTs; } - ListenableFuture>> future = Futures.allAsList(futures); - return Futures.transform(future, new Function>, List>() { - @Nullable - @Override - public List apply(@Nullable List> results) { - if (results == null || results.isEmpty()) { - return null; - } - return results.stream() - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - } - }, service); + return getTskvEntriesFuture(Futures.allAsList(futures)); } } - private ListenableFuture> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) { + private ListenableFuture> findAndAggregateAsync(TenantId tenantId, EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) { List> entitiesFutures = new ArrayList<>(); String entityIdStr = fromTimeUUID(entityId.getId()); - switch (aggregation) { - case AVG: - entitiesFutures.add(tsKvRepository.findAvg( - entityIdStr, - entityId.getEntityType(), - key, - startTs, - endTs)); - - break; - case MAX: - entitiesFutures.add(tsKvRepository.findStringMax( - entityIdStr, - entityId.getEntityType(), - key, - startTs, - endTs)); - entitiesFutures.add(tsKvRepository.findNumericMax( - entityIdStr, - entityId.getEntityType(), - key, - startTs, - endTs)); - - break; - case MIN: - entitiesFutures.add(tsKvRepository.findStringMin( - entityIdStr, - entityId.getEntityType(), - key, - startTs, - endTs)); - entitiesFutures.add(tsKvRepository.findNumericMin( - entityIdStr, - entityId.getEntityType(), - key, - startTs, - endTs)); - break; - case SUM: - entitiesFutures.add(tsKvRepository.findSum( - entityIdStr, - entityId.getEntityType(), - key, - startTs, - endTs)); - break; - case COUNT: - entitiesFutures.add(tsKvRepository.findCount( - entityIdStr, - entityId.getEntityType(), - key, - startTs, - endTs)); - - break; - default: - throw new IllegalArgumentException("Not supported aggregation type: " + aggregation); - } + switchAgregation(entityId, key, startTs, endTs, aggregation, entitiesFutures, entityIdStr); SettableFuture listenableFuture = SettableFuture.create(); - CompletableFuture> entities = CompletableFuture.allOf(entitiesFutures.toArray(new CompletableFuture[entitiesFutures.size()])) - .thenApply(v -> entitiesFutures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList())); - + .thenApply(v -> entitiesFutures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); entities.whenComplete((tsKvEntities, throwable) -> { if (throwable != null) { @@ -247,22 +118,98 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp listenableFuture.set(result); } }); - return Futures.transform(listenableFuture, new Function>() { - @Override - public Optional apply(@Nullable TsKvEntity entity) { - if (entity != null && entity.isNotEmpty()) { - entity.setEntityId(entityIdStr); - entity.setEntityType(entityId.getEntityType()); - entity.setKey(key); - entity.setTs(ts); - return Optional.of(DaoUtil.getData(entity)); - } else { - return Optional.empty(); - } + return Futures.transform(listenableFuture, entity -> { + if (entity != null && entity.isNotEmpty()) { + entity.setEntityId(entityIdStr); + entity.setEntityType(entityId.getEntityType()); + entity.setKey(key); + entity.setTs(ts); + return Optional.of(DaoUtil.getData(entity)); + } else { + return Optional.empty(); } }); } + private void switchAgregation(EntityId entityId, String key, long startTs, long endTs, Aggregation aggregation, List> entitiesFutures, String entityIdStr) { + switch (aggregation) { + case AVG: + findAvg(entityId, key, startTs, endTs, entitiesFutures, entityIdStr); + break; + case MAX: + findMax(entityId, key, startTs, endTs, entitiesFutures, entityIdStr); + break; + case MIN: + findMin(entityId, key, startTs, endTs, entitiesFutures, entityIdStr); + break; + case SUM: + findSum(entityId, key, startTs, endTs, entitiesFutures, entityIdStr); + break; + case COUNT: + findCount(entityId, key, startTs, endTs, entitiesFutures, entityIdStr); + break; + default: + throw new IllegalArgumentException("Not supported aggregation type: " + aggregation); + } + } + + private void findCount(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures, String entityIdStr) { + entitiesFutures.add(tsKvRepository.findCount( + entityIdStr, + entityId.getEntityType(), + key, + startTs, + endTs)); + } + + private void findSum(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures, String entityIdStr) { + entitiesFutures.add(tsKvRepository.findSum( + entityIdStr, + entityId.getEntityType(), + key, + startTs, + endTs)); + } + + private void findMin(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures, String entityIdStr) { + entitiesFutures.add(tsKvRepository.findStringMin( + entityIdStr, + entityId.getEntityType(), + key, + startTs, + endTs)); + entitiesFutures.add(tsKvRepository.findNumericMin( + entityIdStr, + entityId.getEntityType(), + key, + startTs, + endTs)); + } + + private void findMax(EntityId entityId, String key, long startTs, long endTs, List