diff --git a/.travis.yml b/.travis.yml index 6f22da9139..f635da5a55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,9 @@ before_install: - export MAVEN_OPTS="-Dmaven.repo.local=$HOME/.m2/repository -Xms1024m -Xmx3072m" - export HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false jdk: - - oraclejdk8 + - openjdk8 language: java sudo: required services: - docker -script: mvn clean verify -Ddockerfile.skip=false -DblackBoxTests.skip=false -DblackBoxTests.skipTailChildContainers=true +script: mvn clean verify -Ddockerfile.skip=false diff --git a/application/pom.xml b/application/pom.xml index 68ae1aa77c..19e1c5c759 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard application diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index d811393c4b..2daf3e27c8 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json @@ -16,10 +16,10 @@ "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('alarms-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}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\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\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStatusFilter\": {\n \"title\": \"Enable alarm status filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\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\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStatusFilter\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\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, alarm, 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,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5}" + "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,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStatusFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}" } } ] -} \ No newline at end of file +} 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 dab5e7976e..d07186fd42 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -15,7 +15,7 @@ "resources": [], "templateHtml": "", "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 \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasoirceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasoirceTitleFontSize = self.ctx.width/12;\n }\n datasoirceTitleFontSize = Math.min(datasoirceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasoirceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\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\":\"Attributes card\"}" @@ -32,9 +32,9 @@ "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 '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}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"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;\"}]}]}" + "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,\"enableSelectColumnDisplay\":true},\"title\":\"Entity 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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}" } }, { @@ -63,7 +63,7 @@ "resources": [], "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml));\n self.ctx.html = '
' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml));\n self.ctx.html = '
' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label == 'entityLabel') {\n variableInfo.isEntityLabel = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && !variableInfo.isEntityLabel && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get('utils');\n var types = $injector.get('types');\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n } else if (variableInfo.isEntityLabel) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityLabel || self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n text = replaceCustomTranslations(text);\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{' + types.translate.i18nPrefix + ':[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"cardHtml\"],\n \"properties\": {\n \"cardCss\": {\n \"title\": \"CSS\",\n \"type\": \"string\",\n \"default\": \".card {\\n font-weight: bold; \\n}\"\n },\n \"cardHtml\": {\n \"title\": \"HTML\",\n \"type\": \"string\",\n \"default\": \"
HTML code here
\"\n }\n }\n },\n \"form\": [\n {\n \"key\": \"cardCss\",\n \"type\": \"css\"\n }, \n {\n \"key\": \"cardHtml\",\n \"type\": \"html\"\n } \n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"
\\n
\\n
\\n

Value title

\\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n \\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"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 2ecd90be28..9c97a03084 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -35,7 +35,7 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" @@ -147,7 +147,7 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" @@ -163,7 +163,7 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 5dfe4d0f7f..44f7d52ebf 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -109,9 +109,9 @@ "sizeX": 4, "sizeY": 2, "resources": [], - "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n {{buttonLable}}\n \n
\n
\n
\n {{ error }}\n
\n
", + "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n {{buttonLabel}}\n \n
\n
\n
\n {{ error }}\n
\n
", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n }\n );\n };\n\n};", + "controllerScript": "self.onInit = function() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n }\n );\n };\n\n};", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Send RPC\"\n },\n \"oneWayElseTwoWay\": {\n \"title\": \"Is One Way Command\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showError\": {\n \"title\": \"Show RPC command execution error\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"methodName\": {\n \"title\": \"RPC method\",\n \"type\": \"string\",\n \"default\": \"rpcCommand\"\n },\n \"methodParams\": {\n \"title\": \"RPC method params\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 5000\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n \"oneWayElseTwoWay\",\n \"showError\",\n \"methodName\",\n {\n \"key\": \"methodParams\",\n \"type\": \"json\"\n },\n \"requestTimeout\",\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -125,9 +125,9 @@ "sizeX": 4, "sizeY": 2, "resources": [], - "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n {{buttonLable}}\n \n
\n
\n
\n {{ error }}\n
\n
", + "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n {{buttonLabel}}\n \n
\n
\n
\n {{ error }}\n
\n
", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n console.log(self.ctx);\n\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n \n attributeService.saveEntityAttributes(\"DEVICE\", self.ctx.defaultSubscription.targetDeviceId,\n entityAttributeType, attributes).then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n console.log(rejection);\n }\n\n );\n };\n\n};", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n console.log(self.ctx);\n\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n \n attributeService.saveEntityAttributes(\"DEVICE\", self.ctx.defaultSubscription.targetDeviceId,\n entityAttributeType, attributes).then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n console.log(rejection);\n }\n\n );\n };\n\n};", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Update device attribute\"\n },\n \"entityAttributeType\": {\n \"title\": \"Device attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"entityParameters\": {\n \"title\": \"Device attribute parameters\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n {\n \"key\": \"entityAttributeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n }, {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }]\n }, {\n \"key\": \"entityParameters\",\n \"type\": \"json\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json index d9490e0c3b..c0d0fa7cdf 100644 --- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json @@ -16,9 +16,9 @@ "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}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\"},\"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 \\n \\n
\\n
Device name is required.
\\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n \\n \\n \\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 \\n \\n
\\n
Device name is required.
\\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n \\n \\n \\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}\"}]}}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"device\",\"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 \\n \\n
\\n
Device name is required.
\\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n \\n \\n \\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 \\n \\n
\\n
Device name is required.
\\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n
\\n
\\n \\n \\n \\n \\n \\n \\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}\"}]}}" } }, { @@ -32,10 +32,10 @@ "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}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\"},\"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 \\n \\n
\\n
Asset name is required.
\\n
\\n
\\n \\n \\n
\\n \\n \\n \\n \\n \\n \\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 \\n \\n
\\n
Asset name is required.
\\n
\\n
\\n \\n \\n
\\n \\n \\n \\n \\n \\n \\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}\"}]}}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"asset\",\"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 \\n \\n
\\n
Asset name is required.
\\n
\\n
\\n \\n \\n
\\n \\n \\n \\n \\n \\n \\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 \\n \\n
\\n
Asset name is required.
\\n
\\n
\\n \\n \\n
\\n \\n \\n \\n \\n \\n \\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 5129ea19a7..2cc297b263 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 @@ -101,6 +101,22 @@ "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_server_location_attribute", + "name": "Update server location attribute", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n my_location\n {{ 'widgets.input-widgets.get-location' | translate }}\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.timeseries-not-allowed' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | 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\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\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;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\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 md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width 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 console.log(self.ctx);\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.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\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 > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.attribute){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\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 key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n 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'), \r\n 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.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\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 for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\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 $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: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\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 \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\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 \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\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 location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, { "alias": "update_shared_string_attribute", "name": "Update shared string attribute", @@ -197,6 +213,22 @@ "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_shared_location_attribute", + "name": "Update shared location attribute", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n my_location\n {{ 'widgets.input-widgets.get-location' | translate }}\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
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | 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\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\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;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\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 md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width 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 console.log(self.ctx);\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.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\r\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\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 > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.attribute){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\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 key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n 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'), \r\n 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.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\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 for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\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 $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: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\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 \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\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 \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\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 location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, { "alias": "update_string_timeseries", "name": "Update string timeseries", @@ -261,6 +293,22 @@ "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_location_timeseries", + "name": "Update location timeseries", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "
\n
\n\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n \n \n \n
\n
{{requiredErrorMessage}}
\n
\n
\n
\n\n
\n \n my_location\n {{ 'widgets.input-widgets.get-location' | translate }}\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.attribute-not-allowed' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | 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\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\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;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\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 md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width 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 console.log(self.ctx);\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.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\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 > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.timeseries){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\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.saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n 'scope',\r\n [{\r\n key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n 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'), \r\n 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.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\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 for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\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 $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: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\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 \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\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 \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\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 \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\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 location timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, { "alias": "update_multiple_attributes", "name": "Update Multiple Attributes", @@ -271,9 +319,9 @@ "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", + "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\r\nself.onResize = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-resize', self.ctx.$scope.formId);\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified (only if action buttons are visible)\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n \"updateAllValues\",\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n \"fieldsInRow\"\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n \"required\",\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"step\",\n \"requiredErrorMessage\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\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\":{}}" } }, @@ -292,6 +340,70 @@ "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\":{}}" } + }, + { + "alias": "device_claiming_widget", + "name": "Device claiming widget", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 4.5, + "resources": [], + "templateHtml": "
\n
\n \n \n \n
\n
{{requiredErrorDevice}}
\n
\n
\n \n \n \n
\n
{{requiredErrorSecretKey}}
\n
\n
\n
\n
\n {{labelClaimButon}}\n
\n
", + "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}", + "controllerScript": "let $scope;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let $q = $injector.get('$q');\n let $http = $injector.get('$http');\n let toast = $scope.$injector.get('toast');\n let utils = $scope.$injector.get('utils');\n let $translate = $scope.$injector.get('$translate');\n let $rootScope = $scope.$injector.get('$rootScope');\n let settings = self.ctx.settings || {};\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n $scope.deviceObj = {};\n \n const config = {\n ignoreErrors: true \n };\n \n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claim = () => {\n $scope.loading = true;\n claimDevice($scope.deviceObj.deviceName, $scope.deviceObj.deviceSecret, config).then(\n (data) => {\n successClaim();\n },\n (error) => {\n $scope.loading = false;\n if(error.status == 404) {\n toast.showError(deviceNotFound, angular.element('.claim-form'),'bottom left');\n } else if(error.status == 400) {\n toast.showError(failedClaimDevice, angular.element('.claim-form'),'bottom left');\n }\n }\n );\n }\n \n function claimDevice(deviceName, deviceSecret, config) {\n let deferred = $q.defer();\n let url = \"/api/customer/device/\" + deviceName + \"/claim\";\n let obj = deviceSecret ? { secretKey: deviceSecret } : {};\n $http.post(url, obj, config).then(\n (payload) => {\n deferred.resolve(payload.data); \n },\n (error) => {\n deferred.reject(error);\n }\n );\n return deferred.promise;\n }\n \n function updateAliasData() {\n var aliasIds = [];\n for (var id in self.ctx.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n var tasks = [];\n aliasIds.forEach(function(aliasId) {\n self.ctx.aliasController.setAliasUnresolved(aliasId);\n tasks.push(self.ctx.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(function() {\n $rootScope.$broadcast('widgetForceReInit');\n });\n }\n \n function successClaim() {\n resetForm();\n $scope.claimDeviceForm.$setPristine();\n $scope.claimDeviceForm.$setUntouched();\n $scope.loading = false;\n toast.showSuccess(successfulClaim, 2000);\n updateAliasData();\n }\n \n function resetForm() {\n $scope.deviceObj = {};\n }\n}\n\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\n }]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + }, + { + "alias": "markers_placement_image_map", + "name": "Markers Placement - Image Map", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":[0,0]},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + } + }, + { + "alias": "markers_placement_openstreetmap", + "name": "Markers Placement - OpenStreetMap", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":[0,0],\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + } + }, + { + "alias": "markers_placement_google_maps", + "name": "Markers Placement - Google Maps", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":[0,0],\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"gridSize\":60,\"defaultZoomLevel\":5},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + } } ] } diff --git a/application/src/main/data/upgrade/2.4.2/schema_update.sql b/application/src/main/data/upgrade/2.4.2/schema_update.sql new file mode 100644 index 0000000000..79bbc91fec --- /dev/null +++ b/application/src/main/data/upgrade/2.4.2/schema_update.sql @@ -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. +-- + +DROP INDEX IF EXISTS idx_alarm_originator_alarm_type; + +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(originator_id, type, start_ts DESC); + +CREATE INDEX IF NOT EXISTS idx_device_customer_id ON device(tenant_id, customer_id); + +CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id, customer_id, type); + +CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type); + +CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id); + +CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type); + +CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 42b66a73e8..f15a8bff63 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -32,6 +32,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.RuleChainTransactionService; @@ -88,6 +90,7 @@ import java.io.StringWriter; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Component @@ -285,6 +288,23 @@ public class ActorSystemContext { @Getter private long statisticsPersistFrequency; + @Getter + private final AtomicInteger jsInvokeRequestsCount = new AtomicInteger(0); + @Getter + private final AtomicInteger jsInvokeResponsesCount = new AtomicInteger(0); + @Getter + private final AtomicInteger jsInvokeFailuresCount = new AtomicInteger(0); + + @Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}") + public void printStats() { + if (statisticsEnabled) { + if (jsInvokeRequestsCount.get() > 0 || jsInvokeResponsesCount.get() > 0 || jsInvokeFailuresCount.get() > 0) { + log.info("Rule Engine JS Invoke Stats: requests [{}] responses [{}] failures [{}]", + jsInvokeRequestsCount.getAndSet(0), jsInvokeResponsesCount.getAndSet(0), jsInvokeFailuresCount.getAndSet(0)); + } + } + } + @Value("${actors.tenant.create_components_on_init}") @Getter private boolean tenantComponentsInitEnabled; @@ -336,6 +356,10 @@ public class ActorSystemContext { @Getter private CassandraBufferedRateExecutor cassandraBufferedRateExecutor; + @Autowired(required = false) + @Getter + private RedisTemplate redisTemplate; + public ActorSystemContext() { config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load()); } diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index aa1f8bc909..6784a2a0f3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -223,6 +223,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } void process(ActorContext context, TransportToDeviceActorMsgWrapper wrapper) { + boolean reportDeviceActivity = false; TransportToDeviceActorMsg msg = wrapper.getMsg(); if (msg.hasSessionEvent()) { processSessionStateMsgs(msg.getSessionInfo(), msg.getSessionEvent()); @@ -235,11 +236,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } if (msg.hasPostAttributes()) { handlePostAttributesRequest(context, msg.getSessionInfo(), msg.getPostAttributes()); - reportLogicalDeviceActivity(); + reportDeviceActivity = true; } if (msg.hasPostTelemetry()) { handlePostTelemetryRequest(context, msg.getSessionInfo(), msg.getPostTelemetry()); - reportLogicalDeviceActivity(); + reportDeviceActivity = true; } if (msg.hasGetAttributes()) { handleGetAttributesRequest(context, msg.getSessionInfo(), msg.getGetAttributes()); @@ -249,11 +250,14 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } if (msg.hasToServerRPCCallRequest()) { handleClientSideRPCRequest(context, msg.getSessionInfo(), msg.getToServerRPCCallRequest()); - reportLogicalDeviceActivity(); + reportDeviceActivity = true; } if (msg.hasSubscriptionInfo()) { handleSessionActivity(context, msg.getSessionInfo(), msg.getSubscriptionInfo()); } + if (reportDeviceActivity) { + reportLogicalDeviceActivity(); + } } private void reportLogicalDeviceActivity() { 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 5cfc2f9a19..e5e326e597 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 @@ -22,8 +22,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.channel.EventLoopGroup; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.StringUtils; -import org.thingsboard.rule.engine.api.ListeningExecutor; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.RuleChainTransactionService; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; @@ -60,7 +61,6 @@ import org.thingsboard.server.dao.customer.CustomerService; 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; @@ -229,6 +229,27 @@ class DefaultTbContext implements TbContext { return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames); } + @Override + public void logJsEvalRequest() { + if (mainCtx.isStatisticsEnabled()) { + mainCtx.getJsInvokeRequestsCount().incrementAndGet(); + } + } + + @Override + public void logJsEvalResponse() { + if (mainCtx.isStatisticsEnabled()) { + mainCtx.getJsInvokeResponsesCount().incrementAndGet(); + } + } + + @Override + public void logJsEvalFailure() { + if (mainCtx.isStatisticsEnabled()) { + mainCtx.getJsInvokeFailuresCount().incrementAndGet(); + } + } + @Override public String getNodeId() { return mainCtx.getNodeIdProvider().getNodeId(); @@ -361,6 +382,16 @@ class DefaultTbContext implements TbContext { return mainCtx.getCassandraBufferedRateExecutor().submit(task); } + @Override + public RedisTemplate getRedisTemplate() { + return mainCtx.getRedisTemplate(); + } + + @Override + public String getServerAddress() { + return mainCtx.getServerAddress(); + } + private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("ruleNodeId", ruleNodeId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java index bc28042fb2..852fbe2884 100644 --- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java +++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java @@ -22,8 +22,10 @@ import akka.actor.Terminated; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; @@ -50,6 +52,7 @@ import org.thingsboard.server.service.cluster.discovery.DiscoveryService; import org.thingsboard.server.service.cluster.discovery.ServerInstance; import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.service.transport.RuleEngineStats; import scala.concurrent.Await; import scala.concurrent.Future; import scala.concurrent.duration.Duration; @@ -58,6 +61,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE; @@ -187,8 +191,29 @@ public class DefaultActorService implements ActorService { this.rpcManagerActor.tell(msg, ActorRef.noSender()); } + @Value("${cluster.stats.enabled:false}") + private boolean statsEnabled; + + private final AtomicInteger sentClusterMsgs = new AtomicInteger(0); + private final AtomicInteger receivedClusterMsgs = new AtomicInteger(0); + + + @Scheduled(fixedDelayString = "${cluster.stats.print_interval_ms}") + public void printStats() { + if (statsEnabled) { + int sent = sentClusterMsgs.getAndSet(0); + int received = receivedClusterMsgs.getAndSet(0); + if (sent > 0 || received > 0) { + log.info("Cluster msgs sent [{}] received [{}]", sent, received); + } + } + } + @Override public void onReceivedMsg(ServerAddress source, ClusterAPIProtos.ClusterMessage msg) { + if (statsEnabled) { + receivedClusterMsgs.incrementAndGet(); + } ServerAddress serverAddress = new ServerAddress(source.getHost(), source.getPort(), source.getServerType()); if (log.isDebugEnabled()) { log.info("Received msg [{}] from [{}]", msg.getMessageType().name(), serverAddress); @@ -239,11 +264,17 @@ public class DefaultActorService implements ActorService { @Override public void onSendMsg(ClusterAPIProtos.ClusterMessage msg) { + if (statsEnabled) { + sentClusterMsgs.incrementAndGet(); + } rpcManagerActor.tell(msg, ActorRef.noSender()); } @Override public void onRpcSessionCreateRequestMsg(RpcSessionCreateRequestMsg msg) { + if (statsEnabled) { + sentClusterMsgs.incrementAndGet(); + } rpcManagerActor.tell(msg, ActorRef.noSender()); } diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java index fc25af3a56..56c0bc1974 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java @@ -84,7 +84,7 @@ public class ThingsboardMessageConfiguration { } @Slf4j - static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader { + public static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader { public static final String NAME = "spring"; 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 a6122dd82e..81415ae35d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -28,12 +28,12 @@ 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.common.data.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; +import org.thingsboard.server.common.data.UpdateMessage; @RestController @RequestMapping("/api/admin") 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 690ba76a0f..f744f5d822 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -24,7 +24,6 @@ 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; @@ -43,14 +42,12 @@ 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.common.data.security.model.SecuritySettings; import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.model.UserPasswordPolicy; +import org.thingsboard.server.common.data.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; diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 3fc809c424..77a9d3ad0f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -516,15 +516,27 @@ public abstract class BaseController { protected String constructBaseUrl(HttpServletRequest request) { String scheme = request.getScheme(); - if (request.getHeader("x-forwarded-proto") != null) { - scheme = request.getHeader("x-forwarded-proto"); + + String forwardedProto = request.getHeader("x-forwarded-proto"); + if (forwardedProto != null) { + scheme = forwardedProto; } + int serverPort = request.getServerPort(); if (request.getHeader("x-forwarded-port") != null) { try { serverPort = request.getIntHeader("x-forwarded-port"); } catch (NumberFormatException e) { } + } else if (forwardedProto != null) { + switch (forwardedProto) { + case "http": + serverPort = 80; + break; + case "https": + serverPort = 443; + break; + } } String baseUrl = String.format("%s://%s:%d", 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 f497c4fefa..698dcfb3f3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; 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.common.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; diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index 8d08179409..1812a4f90f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -24,7 +24,6 @@ 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.Event; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -32,9 +31,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.service.security.permission.Operation; -import org.thingsboard.server.service.security.permission.Resource; @RestController @RequestMapping("/api") 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 f384817465..56c90a2dab 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -119,6 +119,11 @@ public class ThingsboardInstallService { case "2.4.0": log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ..."); + case "2.4.1": + log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ..."); + + databaseUpgradeService.upgradeDatabase("2.4.1"); + log.info("Updating system data..."); systemDataLoaderService.deleteSystemWidgetBundle("charts"); @@ -133,6 +138,7 @@ public class ThingsboardInstallService { systemDataLoaderService.deleteSystemWidgetBundle("gateway_widgets"); systemDataLoaderService.deleteSystemWidgetBundle("input_widgets"); systemDataLoaderService.deleteSystemWidgetBundle("date"); + systemDataLoaderService.deleteSystemWidgetBundle("entity_admin_widgets"); systemDataLoaderService.loadSystemWidgets(); break; diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java index 425b578f7b..dabca5f3d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java @@ -30,13 +30,6 @@ public interface ClusterRoutingService extends DiscoveryServiceListener { ServerAddress getCurrentServer(); - Optional resolveByUuid(UUID uuid); - Optional resolveById(EntityId entityId); - Optional resolveByUuid(ServerType server, UUID uuid); - - Optional resolveById(ServerType server, EntityId entityId); - - } diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java index 87948eddba..99051389c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java @@ -88,21 +88,6 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService { return resolveByUuid(rootCircle, entityId.getId()); } - @Override - public Optional resolveByUuid(UUID uuid) { - return resolveByUuid(rootCircle, uuid); - } - - @Override - public Optional resolveByUuid(ServerType server, UUID uuid) { - return resolveByUuid(circles[server.ordinal()], uuid); - } - - @Override - public Optional resolveById(ServerType server, EntityId entityId) { - return resolveByUuid(circles[server.ordinal()], entityId.getId()); - } - private Optional resolveByUuid(ConsistentHashCircle circle, UUID uuid) { Assert.notNull(uuid); if (circle.isEmpty()) { diff --git a/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java index 82604b63a3..1a4c654ea3 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; @Component public class ClusterRpcCallbackExecutorService extends AbstractListeningExecutor { diff --git a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java index 40b4302d16..2c8678fd45 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; @Component public class DbCallbackExecutorService extends AbstractListeningExecutor { diff --git a/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java index 200cfef090..f61e0bd450 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; @Component public class ExternalCallExecutorService extends AbstractListeningExecutor { diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java index c73c12e65f..68ca728823 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java @@ -267,6 +267,15 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { } catch (InvalidQueryException e) {} log.info("Schema updated."); break; + case "2.4.1": + log.info("Updating schema ..."); + String updateAssetTableStmt = "alter table asset add label text"; + try { + cluster.getSession().execute(updateAssetTableStmt); + Thread.sleep(2500); + } catch (InvalidQueryException e) {} + log.info("Schema updated."); + break; default: throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index d086e4be8b..3210742388 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -176,6 +176,26 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService { log.info("Schema updated."); } break; + case "2.4.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + try { + conn.createStatement().execute("ALTER TABLE asset ADD COLUMN label varchar(255)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) {} + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.2", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + try { + conn.createStatement().execute("ALTER TABLE device ADD CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) {} + try { + conn.createStatement().execute("ALTER TABLE device_credentials ADD CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) {} + try { + conn.createStatement().execute("ALTER TABLE asset ADD CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) {} + log.info("Schema updated."); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java index b56d884331..9a114dbba5 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java @@ -17,7 +17,7 @@ package org.thingsboard.server.service.mail; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.thingsboard.server.service.executors.AbstractListeningExecutor; +import org.thingsboard.common.util.AbstractListeningExecutor; @Component public class MailExecutorService extends AbstractListeningExecutor { diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java index 1252448b16..334e1b8772 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java @@ -17,7 +17,7 @@ package org.thingsboard.server.service.script; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.thingsboard.server.service.executors.AbstractListeningExecutor; +import org.thingsboard.common.util.AbstractListeningExecutor; @Component public class JsExecutorService extends AbstractListeningExecutor { diff --git a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java index 4f73a59961..00c50940c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.script; +import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.Getter; @@ -22,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.kafka.TBKafkaConsumerTemplate; @@ -29,13 +31,16 @@ import org.thingsboard.server.kafka.TBKafkaProducerTemplate; import org.thingsboard.server.kafka.TbKafkaRequestTemplate; import org.thingsboard.server.kafka.TbKafkaSettings; import org.thingsboard.server.kafka.TbNodeIdProvider; -import org.thingsboard.server.service.cluster.discovery.DiscoveryService; +import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; @Slf4j @ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "remote", matchIfMissing = true) @@ -70,6 +75,30 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { @Value("${js.remote.max_errors}") private int maxErrors; + @Value("${js.remote.stats.enabled:false}") + private boolean statsEnabled; + + private final AtomicInteger kafkaPushedMsgs = new AtomicInteger(0); + private final AtomicInteger kafkaInvokeMsgs = new AtomicInteger(0); + private final AtomicInteger kafkaEvalMsgs = new AtomicInteger(0); + private final AtomicInteger kafkaFailedMsgs = new AtomicInteger(0); + private final AtomicInteger kafkaTimeoutMsgs = new AtomicInteger(0); + + @Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}") + public void printStats() { + if (statsEnabled) { + int pushedMsgs = kafkaPushedMsgs.getAndSet(0); + int invokeMsgs = kafkaInvokeMsgs.getAndSet(0); + int evalMsgs = kafkaEvalMsgs.getAndSet(0); + int failed = kafkaFailedMsgs.getAndSet(0); + int timedOut = kafkaTimeoutMsgs.getAndSet(0); + if (pushedMsgs > 0 || invokeMsgs > 0 || evalMsgs > 0 || failed > 0 || timedOut > 0) { + log.info("Kafka JS Invoke Stats: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]", + pushedMsgs, invokeMsgs + evalMsgs, invokeMsgs, evalMsgs, failed, timedOut); + } + } + } + private TbKafkaRequestTemplate kafkaTemplate; private Map scriptIdToBodysMap = new ConcurrentHashMap<>(); @@ -80,22 +109,6 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { requestBuilder.clientId("producer-js-invoke-" + nodeIdProvider.getNodeId()); requestBuilder.defaultTopic(requestTopic); requestBuilder.encoder(new RemoteJsRequestEncoder()); - requestBuilder.enricher((request, responseTopic, requestId) -> { - JsInvokeProtos.RemoteJsRequest.Builder remoteRequest = JsInvokeProtos.RemoteJsRequest.newBuilder(); - if (request.hasCompileRequest()) { - remoteRequest.setCompileRequest(request.getCompileRequest()); - } - if (request.hasInvokeRequest()) { - remoteRequest.setInvokeRequest(request.getInvokeRequest()); - } - if (request.hasReleaseRequest()) { - remoteRequest.setReleaseRequest(request.getReleaseRequest()); - } - remoteRequest.setResponseTopic(responseTopic); - remoteRequest.setRequestIdMSB(requestId.getMostSignificantBits()); - remoteRequest.setRequestIdLSB(requestId.getLeastSignificantBits()); - return remoteRequest.build(); - }); TBKafkaConsumerTemplate.TBKafkaConsumerTemplateBuilder responseBuilder = TBKafkaConsumerTemplate.builder(); responseBuilder.settings(kafkaSettings); @@ -138,7 +151,22 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { .build(); log.trace("Post compile request for scriptId [{}]", scriptId); - ListenableFuture future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper); + ListenableFuture future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper); + kafkaPushedMsgs.incrementAndGet(); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable JsInvokeProtos.RemoteJsResponse result) { + kafkaEvalMsgs.incrementAndGet(); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { + kafkaTimeoutMsgs.incrementAndGet(); + } + kafkaFailedMsgs.incrementAndGet(); + } + }); return Futures.transform(future, response -> { JsInvokeProtos.JsCompileResponse compilationResult = response.getCompileResponse(); UUID compiledScriptId = new UUID(compilationResult.getScriptIdMSB(), compilationResult.getScriptIdLSB()); @@ -174,7 +202,22 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { .setInvokeRequest(jsRequestBuilder.build()) .build(); - ListenableFuture future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper); + ListenableFuture future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper); + kafkaPushedMsgs.incrementAndGet(); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable JsInvokeProtos.RemoteJsResponse result) { + kafkaInvokeMsgs.incrementAndGet(); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { + kafkaTimeoutMsgs.incrementAndGet(); + } + kafkaFailedMsgs.incrementAndGet(); + } + }); return Futures.transform(future, response -> { JsInvokeProtos.JsInvokeResponse invokeResult = response.getInvokeResponse(); if (invokeResult.getSuccess()) { @@ -197,7 +240,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { .setReleaseRequest(jsRequest) .build(); - ListenableFuture future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper); + ListenableFuture future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper); JsInvokeProtos.RemoteJsResponse response = future.get(); JsInvokeProtos.JsReleaseResponse compilationResult = response.getReleaseResponse(); diff --git a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java index db1a75add5..d42d221d24 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java @@ -15,15 +15,23 @@ */ package org.thingsboard.server.service.script; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.kafka.TbKafkaEncoder; +import java.nio.charset.StandardCharsets; + /** * Created by ashvayka on 25.09.18. */ public class RemoteJsRequestEncoder implements TbKafkaEncoder { @Override public byte[] encode(JsInvokeProtos.RemoteJsRequest value) { - return value.toByteArray(); + try { + return JsonFormat.printer().print(value).getBytes(StandardCharsets.UTF_8); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java index 0ac7e6f647..8407ceaaa1 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.service.script; +import com.google.protobuf.util.JsonFormat; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.kafka.TbKafkaDecoder; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * Created by ashvayka on 25.09.18. @@ -27,6 +29,8 @@ public class RemoteJsResponseDecoder implements TbKafkaDecoder executeUpdateAsync(TbMsg msg) { + ListenableFuture result = executeScriptAsync(msg); + return Futures.transformAsync(result, json -> { + if (!json.isObject()) { + log.warn("Wrong result type: {}", json.getNodeType()); + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); + } else { + return Futures.immediateFuture(unbindMsg(json, msg)); + } + }); + } + @Override public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException { JsonNode result = executeScript(prevMsg); @@ -144,6 +159,19 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S return result.asBoolean(); } + @Override + public ListenableFuture executeFilterAsync(TbMsg msg) { + ListenableFuture result = executeScriptAsync(msg); + return Futures.transformAsync(result, json -> { + if (!json.isBoolean()) { + log.warn("Wrong result type: {}", json.getNodeType()); + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); + } else { + return Futures.immediateFuture(json.asBoolean()); + } + }); + } + @Override public Set executeSwitch(TbMsg msg) throws ScriptException { JsonNode result = executeScript(msg); @@ -173,7 +201,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S return mapper.readTree(eval); } catch (ExecutionException e) { if (e.getCause() instanceof ScriptException) { - throw (ScriptException)e.getCause(); + throw (ScriptException) e.getCause(); } else if (e.getCause() instanceof RuntimeException) { throw new ScriptException(e.getCause().getMessage()); } else { @@ -184,6 +212,24 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S } } + private ListenableFuture executeScriptAsync(TbMsg msg) { + String[] inArgs = prepareArgs(msg); + return Futures.transformAsync(sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]), + o -> { + try { + return Futures.immediateFuture(mapper.readTree(o.toString())); + } catch (Exception e) { + if (e.getCause() instanceof ScriptException) { + return Futures.immediateFailedFuture(e.getCause()); + } else if (e.getCause() instanceof RuntimeException) { + return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage())); + } else { + return Futures.immediateFailedFuture(new ScriptException(e)); + } + } + }); + } + public void destroy() { sandboxService.release(this.scriptId); } 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 index 94d0fea86f..3eb4d115e3 100644 --- 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 @@ -42,14 +42,13 @@ 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 org.thingsboard.server.common.data.security.model.SecuritySettings; +import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import javax.annotation.Resource; import java.util.ArrayList; 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 index 50265863b3..1425600cc4 100644 --- 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 @@ -19,7 +19,7 @@ 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; +import org.thingsboard.server.common.data.security.model.SecuritySettings; public interface SystemSecurityService { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 37d6473d53..c3c8dc7803 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -30,7 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.RpcError; +import org.springframework.util.StringUtils; import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -38,8 +38,13 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -49,22 +54,16 @@ import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.cluster.ClusterAPIProtos; import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; -import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; @@ -92,7 +91,8 @@ public class DefaultDeviceStateService implements DeviceStateService { public static final String INACTIVITY_ALARM_TIME = "inactivityAlarmTime"; public static final String INACTIVITY_TIMEOUT = "inactivityTimeout"; - public static final List PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT); + public static final List PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, + LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT); @Autowired private TenantService tenantService; @@ -103,6 +103,9 @@ public class DefaultDeviceStateService implements DeviceStateService { @Autowired private AttributesService attributesService; + @Autowired + private TimeseriesService tsService; + @Autowired @Lazy private ActorService actorService; @@ -122,19 +125,19 @@ public class DefaultDeviceStateService implements DeviceStateService { @Value("${state.defaultStateCheckIntervalInSec}") @Getter - private long defaultStateCheckIntervalInSec; + private int defaultStateCheckIntervalInSec; -// TODO in v2.1 -// @Value("${state.defaultStatePersistenceIntervalInSec}") -// @Getter -// private long defaultStatePersistenceIntervalInSec; -// -// @Value("${state.defaultStatePersistencePack}") -// @Getter -// private long defaultStatePersistencePack; + @Value("${state.persistToTelemetry:false}") + @Getter + private boolean persistToTelemetry; - private ListeningScheduledExecutorService queueExecutor; + @Value("${state.initFetchPackSize:1000}") + @Getter + private int initFetchPackSize; + private volatile boolean clusterUpdatePending = false; + + private ListeningScheduledExecutorService queueExecutor; private ConcurrentMap> tenantDevices = new ConcurrentHashMap<>(); private ConcurrentMap deviceStates = new ConcurrentHashMap<>(); @@ -143,7 +146,7 @@ public class DefaultDeviceStateService implements DeviceStateService { // Should be always single threaded due to absence of locks. queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor()); queueExecutor.submit(this::initStateFromDB); - queueExecutor.scheduleAtFixedRate(this::updateState, defaultStateCheckIntervalInSec, defaultStateCheckIntervalInSec, TimeUnit.SECONDS); + queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS); //TODO: schedule persistence in v2.1; } @@ -191,7 +194,10 @@ public class DefaultDeviceStateService implements DeviceStateService { @Override public void onClusterUpdate() { - queueExecutor.submit(this::onClusterUpdateSync); + if (!clusterUpdatePending) { + clusterUpdatePending = true; + queueExecutor.submit(this::onClusterUpdateSync); + } } @Override @@ -219,46 +225,59 @@ public class DefaultDeviceStateService implements DeviceStateService { } private void onClusterUpdateSync() { + clusterUpdatePending = false; List tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData(); for (Tenant tenant : tenants) { List> fetchFutures = new ArrayList<>(); - List devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData(); - for (Device device : devices) { - if (!routingService.resolveById(device.getId()).isPresent()) { - if (!deviceStates.containsKey(device.getId())) { - fetchFutures.add(fetchDeviceState(device)); + TextPageLink pageLink = new TextPageLink(initFetchPackSize); + while (pageLink != null) { + TextPageData page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink); + pageLink = page.getNextPageLink(); + for (Device device : page.getData()) { + if (!routingService.resolveById(device.getId()).isPresent()) { + if (!deviceStates.containsKey(device.getId())) { + fetchFutures.add(fetchDeviceState(device)); + } + } else { + Set tenantDeviceSet = tenantDevices.get(tenant.getId()); + if (tenantDeviceSet != null) { + tenantDeviceSet.remove(device.getId()); + } + deviceStates.remove(device.getId()); } - } else { - Set tenantDeviceSet = tenantDevices.get(tenant.getId()); - if (tenantDeviceSet != null) { - tenantDeviceSet.remove(device.getId()); - } - deviceStates.remove(device.getId()); } - } - try { - Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState); - } catch (InterruptedException | ExecutionException e) { - log.warn("Failed to init device state service from DB", e); + try { + Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState); + } catch (InterruptedException | ExecutionException e) { + log.warn("Failed to init device state service from DB", e); + } } } } private void initStateFromDB() { - List tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData(); - for (Tenant tenant : tenants) { - List> fetchFutures = new ArrayList<>(); - List devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData(); - for (Device device : devices) { - if (!routingService.resolveById(device.getId()).isPresent()) { - fetchFutures.add(fetchDeviceState(device)); + try { + List tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData(); + for (Tenant tenant : tenants) { + List> fetchFutures = new ArrayList<>(); + TextPageLink pageLink = new TextPageLink(initFetchPackSize); + while (pageLink != null) { + TextPageData page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink); + pageLink = page.getNextPageLink(); + for (Device device : page.getData()) { + if (!routingService.resolveById(device.getId()).isPresent()) { + fetchFutures.add(fetchDeviceState(device)); + } + } + try { + Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState); + } catch (InterruptedException | ExecutionException e) { + log.warn("Failed to init device state service from DB", e); + } } } - try { - Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState); - } catch (InterruptedException | ExecutionException e) { - log.warn("Failed to init device state service from DB", e); - } + } catch (Throwable t) { + log.warn("Failed to init device states from DB", t); } } @@ -270,15 +289,21 @@ public class DefaultDeviceStateService implements DeviceStateService { private void updateState() { long ts = System.currentTimeMillis(); Set deviceIds = new HashSet<>(deviceStates.keySet()); + log.info("Calculating state updates for {} devices", deviceStates.size()); for (DeviceId deviceId : deviceIds) { - DeviceStateData stateData = deviceStates.get(deviceId); - DeviceState state = stateData.getState(); - state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout()); - if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) { - state.setLastInactivityAlarmTime(ts); - pushRuleEngineMessage(stateData, INACTIVITY_EVENT); - saveAttribute(deviceId, INACTIVITY_ALARM_TIME, ts); - saveAttribute(deviceId, ACTIVITY_STATE, state.isActive()); + DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); + if (stateData != null) { + DeviceState state = stateData.getState(); + state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout()); + if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) { + state.setLastInactivityAlarmTime(ts); + pushRuleEngineMessage(stateData, INACTIVITY_EVENT); + save(deviceId, INACTIVITY_ALARM_TIME, ts); + save(deviceId, ACTIVITY_STATE, state.isActive()); + } + } else { + log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId); + deviceStates.remove(deviceId); } } } @@ -289,7 +314,7 @@ public class DefaultDeviceStateService implements DeviceStateService { long ts = System.currentTimeMillis(); stateData.getState().setLastConnectTime(ts); pushRuleEngineMessage(stateData, CONNECT_EVENT); - saveAttribute(deviceId, LAST_CONNECT_TIME, ts); + save(deviceId, LAST_CONNECT_TIME, ts); } } @@ -299,7 +324,7 @@ public class DefaultDeviceStateService implements DeviceStateService { long ts = System.currentTimeMillis(); stateData.getState().setLastDisconnectTime(ts); pushRuleEngineMessage(stateData, DISCONNECT_EVENT); - saveAttribute(deviceId, LAST_DISCONNECT_TIME, ts); + save(deviceId, LAST_DISCONNECT_TIME, ts); } } @@ -308,11 +333,14 @@ public class DefaultDeviceStateService implements DeviceStateService { if (stateData != null) { DeviceState state = stateData.getState(); long ts = System.currentTimeMillis(); - state.setActive(true); stateData.getState().setLastActivityTime(ts); pushRuleEngineMessage(stateData, ACTIVITY_EVENT); - saveAttribute(deviceId, LAST_ACTIVITY_TIME, ts); - saveAttribute(deviceId, ACTIVITY_STATE, state.isActive()); + save(deviceId, LAST_ACTIVITY_TIME, ts); + + if (!state.isActive()) { + state.setActive(true); + save(deviceId, ACTIVITY_STATE, state.isActive()); + } } } @@ -324,6 +352,7 @@ public class DefaultDeviceStateService implements DeviceStateService { if (device != null) { try { deviceStateData = fetchDeviceState(device).get(); + deviceStates.putIfAbsent(deviceId, deviceStateData); } catch (InterruptedException | ExecutionException e) { log.debug("[{}] Failed to fetch device state!", deviceId, e); } @@ -345,7 +374,7 @@ public class DefaultDeviceStateService implements DeviceStateService { boolean oldActive = state.isActive(); state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout()); if (!oldActive && state.isActive() || oldActive && !state.isActive()) { - saveAttribute(deviceId, ACTIVITY_STATE, state.isActive()); + save(deviceId, ACTIVITY_STATE, state.isActive()); } } } @@ -414,39 +443,55 @@ public class DefaultDeviceStateService implements DeviceStateService { } private ListenableFuture fetchDeviceState(Device device) { - ListenableFuture> attributes = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES); - return Futures.transform(attributes, new Function, DeviceStateData>() { + if (persistToTelemetry) { + ListenableFuture> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES); + return Futures.transform(tsData, extractDeviceStateData(device)); + } else { + ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES); + return Futures.transform(attrData, extractDeviceStateData(device)); + } + } + + private Function, DeviceStateData> extractDeviceStateData(Device device) { + return new Function, DeviceStateData>() { @Nullable @Override - public DeviceStateData apply(@Nullable List attributes) { - long lastActivityTime = getAttributeValue(attributes, LAST_ACTIVITY_TIME, 0L); - long inactivityAlarmTime = getAttributeValue(attributes, INACTIVITY_ALARM_TIME, 0L); - long inactivityTimeout = getAttributeValue(attributes, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec)); - boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout; - DeviceState deviceState = DeviceState.builder() - .active(active) - .lastConnectTime(getAttributeValue(attributes, LAST_CONNECT_TIME, 0L)) - .lastDisconnectTime(getAttributeValue(attributes, LAST_DISCONNECT_TIME, 0L)) - .lastActivityTime(lastActivityTime) - .lastInactivityAlarmTime(inactivityAlarmTime) - .inactivityTimeout(inactivityTimeout) - .build(); - TbMsgMetaData md = new TbMsgMetaData(); - md.putValue("deviceName", device.getName()); - md.putValue("deviceType", device.getType()); - return DeviceStateData.builder() - .tenantId(device.getTenantId()) - .deviceId(device.getId()) - .metaData(md) - .state(deviceState).build(); + public DeviceStateData apply(@Nullable List data) { + try { + long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L); + long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L); + long inactivityTimeout = getEntryValue(data, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec)); + boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout; + DeviceState deviceState = DeviceState.builder() + .active(active) + .lastConnectTime(getEntryValue(data, LAST_CONNECT_TIME, 0L)) + .lastDisconnectTime(getEntryValue(data, LAST_DISCONNECT_TIME, 0L)) + .lastActivityTime(lastActivityTime) + .lastInactivityAlarmTime(inactivityAlarmTime) + .inactivityTimeout(inactivityTimeout) + .build(); + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("deviceName", device.getName()); + md.putValue("deviceType", device.getType()); + return DeviceStateData.builder() + .tenantId(device.getTenantId()) + .deviceId(device.getId()) + .metaData(md) + .state(deviceState).build(); + } catch (Exception e) { + log.warn("[{}] Failed to fetch device state data", device.getId(), e); + throw new RuntimeException(e); + } } - }); + }; } - private long getAttributeValue(List attributes, String attributeName, long defaultValue) { - for (AttributeKvEntry attribute : attributes) { - if (attribute.getKey().equals(attributeName)) { - return attribute.getLongValue().orElse(defaultValue); + private long getEntryValue(List kvEntries, String attributeName, long defaultValue) { + if (kvEntries != null) { + for (KvEntry entry : kvEntries) { + if (entry != null && !StringUtils.isEmpty(entry.getKey()) && entry.getKey().equals(attributeName)) { + return entry.getLongValue().orElse(defaultValue); + } } } return defaultValue; @@ -464,12 +509,26 @@ public class DefaultDeviceStateService implements DeviceStateService { } } - private void saveAttribute(DeviceId deviceId, String key, long value) { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value)); + private void save(DeviceId deviceId, String key, long value) { + if (persistToTelemetry) { + tsSubService.saveAndNotify( + TenantId.SYS_TENANT_ID, deviceId, + Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))), + new AttributeSaveCallback(deviceId, key, value)); + } else { + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value)); + } } - private void saveAttribute(DeviceId deviceId, String key, boolean value) { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value)); + private void save(DeviceId deviceId, String key, boolean value) { + if (persistToTelemetry) { + tsSubService.saveAndNotify( + TenantId.SYS_TENANT_ID, deviceId, + Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), + new AttributeSaveCallback(deviceId, key, value)); + } else { + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value)); + } } private class AttributeSaveCallback implements FutureCallback { diff --git a/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java b/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java index 432d0b2bc6..b40e2b93fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java +++ b/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java @@ -15,14 +15,12 @@ */ package org.thingsboard.server.service.transaction; -import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.RuleChainTransactionService; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.gen.cluster.ClusterAPIProtos; @@ -34,7 +32,6 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Optional; import java.util.Queue; -import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -111,29 +108,18 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ @Override public void endTransaction(TbMsg msg, Consumer onSuccess, Consumer onFailure) { - EntityId originatorId = msg.getTransactionData().getOriginatorId(); - UUID transactionId = msg.getTransactionData().getTransactionId(); - - Optional address = routingService.resolveById(originatorId); + Optional address = routingService.resolveById(msg.getTransactionData().getOriginatorId()); if (address.isPresent()) { - sendTransactionEventToRemoteServer(originatorId, transactionId, address.get()); + sendTransactionEventToRemoteServer(msg, address.get()); executeOnSuccess(onSuccess, msg); } else { - endLocalTransaction(transactionId, originatorId, onSuccess, onFailure); + endLocalTransaction(msg, onSuccess, onFailure); } } @Override public void onRemoteTransactionMsg(ServerAddress serverAddress, byte[] data) { - ClusterAPIProtos.TransactionEndServiceMsgProto proto; - try { - proto = ClusterAPIProtos.TransactionEndServiceMsgProto.parseFrom(data); - } catch (InvalidProtocolBufferException e) { - throw new RuntimeException(e); - } - EntityId originatorId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getOriginatorIdMSB(), proto.getOriginatorIdLSB())); - UUID transactionId = new UUID(proto.getTransactionIdMSB(), proto.getTransactionIdLSB()); - endLocalTransaction(transactionId, originatorId, msg -> { + endLocalTransaction(TbMsg.fromBytes(data), msg -> { }, error -> { }); } @@ -144,21 +130,21 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ log.trace("Added msg to queue, size: [{}]", queue.size()); } - private void endLocalTransaction(UUID transactionId, EntityId originatorId, Consumer onSuccess, Consumer onFailure) { + private void endLocalTransaction(TbMsg msg, Consumer onSuccess, Consumer onFailure) { transactionLock.lock(); try { - BlockingQueue queue = transactionMap.computeIfAbsent(originatorId, id -> + BlockingQueue queue = transactionMap.computeIfAbsent(msg.getTransactionData().getOriginatorId(), id -> new LinkedBlockingQueue<>(finalQueueSize)); TbTransactionTask currentTransactionTask = queue.peek(); if (currentTransactionTask != null) { - if (currentTransactionTask.getMsg().getTransactionData().getTransactionId().equals(transactionId)) { + if (currentTransactionTask.getMsg().getTransactionData().getTransactionId().equals(msg.getTransactionData().getTransactionId())) { currentTransactionTask.setCompleted(true); queue.poll(); log.trace("Removed msg from queue, size [{}]", queue.size()); executeOnSuccess(currentTransactionTask.getOnEnd(), currentTransactionTask.getMsg()); - executeOnSuccess(onSuccess, currentTransactionTask.getMsg()); + executeOnSuccess(onSuccess, msg); TbTransactionTask nextTransactionTask = queue.peek(); if (nextTransactionTask != null) { @@ -247,14 +233,8 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ callbackExecutor.executeAsync(task); } - private void sendTransactionEventToRemoteServer(EntityId entityId, UUID transactionId, ServerAddress address) { - log.trace("[{}][{}] Originator is monitored on other server: {}", entityId, transactionId, address); - ClusterAPIProtos.TransactionEndServiceMsgProto.Builder builder = ClusterAPIProtos.TransactionEndServiceMsgProto.newBuilder(); - builder.setEntityType(entityId.getEntityType().name()); - builder.setOriginatorIdMSB(entityId.getId().getMostSignificantBits()); - builder.setOriginatorIdLSB(entityId.getId().getLeastSignificantBits()); - builder.setTransactionIdMSB(transactionId.getMostSignificantBits()); - builder.setTransactionIdLSB(transactionId.getLeastSignificantBits()); - clusterRpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TRANSACTION_SERVICE_MESSAGE, builder.build().toByteArray()); + private void sendTransactionEventToRemoteServer(TbMsg msg, ServerAddress address) { + log.trace("[{}][{}] Originator is monitored on other server: {}", msg.getTransactionData().getOriginatorId(), msg.getTransactionData().getTransactionId(), address); + clusterRpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TRANSACTION_SERVICE_MESSAGE, TbMsg.toByteArray(msg)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java index 4c0f9bdfee..1c1c7be483 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.transport; import akka.actor.ActorRef; -import com.fasterxml.jackson.databind.ObjectMapper; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.BlockingBucket; import io.github.bucket4j.Bucket4j; @@ -30,11 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.gen.transport.TransportProtos.DeviceActorToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -44,7 +42,6 @@ import org.thingsboard.server.kafka.TBKafkaConsumerTemplate; import org.thingsboard.server.kafka.TBKafkaProducerTemplate; import org.thingsboard.server.kafka.TbKafkaSettings; import org.thingsboard.server.kafka.TbNodeIdProvider; -import org.thingsboard.server.service.cluster.discovery.DiscoveryService; import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; import org.thingsboard.server.service.encoding.DataDecodingEncodingService; @@ -83,6 +80,8 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ private long pollRecordsPerSecond; @Value("${transport.remote.rule_engine.max_poll_records_per_minute}") private long pollRecordsPerMinute; + @Value("${transport.remote.rule_engine.stats.enabled:false}") + private boolean statsEnabled; @Autowired private TbKafkaSettings kafkaSettings; @@ -108,6 +107,8 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ private volatile boolean stopped = false; + private final RuleEngineStats stats = new RuleEngineStats(); + @PostConstruct public void init() { TBKafkaProducerTemplate.TBKafkaProducerTemplateBuilder notificationsProducerBuilder = TBKafkaProducerTemplate.builder(); @@ -176,6 +177,13 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ }); } + @Scheduled(fixedDelayString = "${transport.remote.rule_engine.stats.print_interval_ms}") + public void printStats() { + if (statsEnabled) { + stats.printStats(); + } + } + @Override public void process(String nodeId, DeviceActorToTransportMsg msg) { process(nodeId, msg, null, null); @@ -191,6 +199,9 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ } private void forwardToDeviceActor(TransportToDeviceActorMsg toDeviceActorMsg) { + if (statsEnabled) { + stats.log(toDeviceActorMsg); + } TransportToDeviceActorMsgWrapper wrapper = new TransportToDeviceActorMsgWrapper(toDeviceActorMsg); Optional address = routingService.resolveById(wrapper.getDeviceId()); if (address.isPresent()) { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java b/application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java new file mode 100644 index 0000000000..13cae48383 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java @@ -0,0 +1,83 @@ +/** + * 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.transport; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public class RuleEngineStats { + + private final AtomicInteger totalCounter = new AtomicInteger(0); + private final AtomicInteger sessionEventCounter = new AtomicInteger(0); + private final AtomicInteger postTelemetryCounter = new AtomicInteger(0); + private final AtomicInteger postAttributesCounter = new AtomicInteger(0); + private final AtomicInteger getAttributesCounter = new AtomicInteger(0); + private final AtomicInteger subscribeToAttributesCounter = new AtomicInteger(0); + private final AtomicInteger subscribeToRPCCounter = new AtomicInteger(0); + private final AtomicInteger toDeviceRPCCallResponseCounter = new AtomicInteger(0); + private final AtomicInteger toServerRPCCallRequestCounter = new AtomicInteger(0); + private final AtomicInteger subscriptionInfoCounter = new AtomicInteger(0); + private final AtomicInteger claimDeviceCounter = new AtomicInteger(0); + + public void log(TransportProtos.TransportToDeviceActorMsg msg) { + totalCounter.incrementAndGet(); + if (msg.hasSessionEvent()) { + sessionEventCounter.incrementAndGet(); + } + if (msg.hasPostTelemetry()) { + postTelemetryCounter.incrementAndGet(); + } + if (msg.hasPostAttributes()) { + postAttributesCounter.incrementAndGet(); + } + if (msg.hasGetAttributes()) { + getAttributesCounter.incrementAndGet(); + } + if (msg.hasSubscribeToAttributes()) { + subscribeToAttributesCounter.incrementAndGet(); + } + if (msg.hasSubscribeToRPC()) { + subscribeToRPCCounter.incrementAndGet(); + } + if (msg.hasToDeviceRPCCallResponse()) { + toDeviceRPCCallResponseCounter.incrementAndGet(); + } + if (msg.hasToServerRPCCallRequest()) { + toServerRPCCallRequestCounter.incrementAndGet(); + } + if (msg.hasSubscriptionInfo()) { + subscriptionInfoCounter.incrementAndGet(); + } + if (msg.hasClaimDevice()) { + claimDeviceCounter.incrementAndGet(); + } + } + + public void printStats() { + int total = totalCounter.getAndSet(0); + if (total > 0) { + log.info("Transport total [{}] sessionEvents [{}] telemetry [{}] attributes [{}] getAttr [{}] subToAttr [{}] subToRpc [{}] toDevRpc [{}] " + + "toServerRpc [{}] subInfo [{}] claimDevice [{}] ", + total, sessionEventCounter.getAndSet(0), postTelemetryCounter.getAndSet(0), + postAttributesCounter.getAndSet(0), getAttributesCounter.getAndSet(0), subscribeToAttributesCounter.getAndSet(0), + subscribeToRPCCounter.getAndSet(0), toDeviceRPCCallResponseCounter.getAndSet(0), + toServerRPCCallRequestCounter.getAndSet(0), subscriptionInfoCounter.getAndSet(0), claimDeviceCounter.getAndSet(0)); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index 4a6fa6f271..14cbf95861 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import org.thingsboard.server.service.update.model.UpdateMessage; +import org.thingsboard.server.common.data.UpdateMessage; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; diff --git a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java index 4ddc6dc3df..016c573399 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.service.update; -import org.thingsboard.server.service.update.model.UpdateMessage; +import org.thingsboard.server.common.data.UpdateMessage; public interface UpdateService { diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java index 7a4648f234..67fbf15be5 100644 --- a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java @@ -19,6 +19,7 @@ import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import java.nio.charset.Charset; +import java.util.Random; /** diff --git a/application/src/main/proto/cluster.proto b/application/src/main/proto/cluster.proto index b04a95fbb0..4ff1359e76 100644 --- a/application/src/main/proto/cluster.proto +++ b/application/src/main/proto/cluster.proto @@ -143,11 +143,3 @@ message DeviceStateServiceMsgProto { bool updated = 6; bool deleted = 7; } - -message TransactionEndServiceMsgProto { - string entityType = 1; - int64 originatorIdMSB = 2; - int64 originatorIdLSB = 3; - int64 transactionIdMSB = 4; - int64 transactionIdLSB = 5; -} diff --git a/application/src/main/proto/jsinvoke.proto b/application/src/main/proto/jsinvoke.proto index d42f3c06c0..4c95acadbd 100644 --- a/application/src/main/proto/jsinvoke.proto +++ b/application/src/main/proto/jsinvoke.proto @@ -26,12 +26,9 @@ enum JsInvokeErrorCode { } message RemoteJsRequest { - string responseTopic = 1; - int64 requestIdMSB = 2; - int64 requestIdLSB = 3; - JsCompileRequest compileRequest = 4; - JsInvokeRequest invokeRequest = 5; - JsReleaseRequest releaseRequest = 6; + JsCompileRequest compileRequest = 1; + JsInvokeRequest invokeRequest = 2; + JsReleaseRequest releaseRequest = 3; } message RemoteJsResponse { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 48e37dd646..fcf62a659b 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -85,6 +85,9 @@ cluster: vitrual_nodes_size: "${CLUSTER_VIRTUAL_NODES_SIZE:16}" # Queue partition id for current node partition_id: "${QUEUE_PARTITION_ID:0}" + stats: + enabled: "${TB_CLUSTER_STATS_ENABLED:false}" + print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}" # Plugins configuration parameters plugins: @@ -183,6 +186,8 @@ cassandra: rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" # set all data types values except target to null for the same ts on save set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}" + # log one of cassandra queries with specified frequency (0 - logging is disabled) + print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" tenant_rate_limits: enabled: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_ENABLED:false}" configuration: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION:1000:1,30000:60}" @@ -194,6 +199,25 @@ sql: 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:200}" + # Specify batch size for persisting attribute updates + attributes: + batch_size: "${SQL_ATTRIBUTES_BATCH_SIZE:10000}" + batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}" + stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}" + ts: + batch_size: "${SQL_TS_BATCH_SIZE:10000}" + batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}" + stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}" + ts_latest: + batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}" + batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" + stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" + ts_timescale: + batch_size: "${SQL_TS_TIMESCALE_BATCH_SIZE:10000}" + batch_max_delay: "${SQL_TS_TIMESCALE_BATCH_MAX_DELAY_MS:100}" + stats_print_interval_ms: "${SQL_TS_TIMESCALE_BATCH_STATS_PRINT_MS:10000}" + # Specify whether to remove null characters from strValue of attributes and timeseries before insert + remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}" # Actor system parameters actors: @@ -208,7 +232,7 @@ actors: timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}" rule: # Specify thread pool size for database request callbacks executor service - db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:1}" + db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:50}" # Specify thread pool size for javascript executor service js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}" # Specify thread pool size for mail sender executor service @@ -234,6 +258,7 @@ actors: statistics: # Enable/disable actor statistics enabled: "${ACTORS_STATISTICS_ENABLED:true}" + js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}" persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}" queue: # Enable/disable persistence of un-processed messages to the queue @@ -278,10 +303,41 @@ redis: # standalone or cluster connection: type: standalone + standalone: host: "${REDIS_HOST:localhost}" port: "${REDIS_PORT:6379}" - db: "${REDIS_DB:0}" - password: "${REDIS_PASSWORD:}" + useDefaultClientConfig: "${REDIS_USE_DEFAULT_CLIENT_CONFIG:true}" + # this value may be used only if you used not default ClientConfig + clientName: "${REDIS_CLIENT_NAME:standalone}" + # this value may be used only if you used not default ClientConfig + connectTimeout: "${REDIS_CLIENT_CONNECT_TIMEOUT:30000}" + # this value may be used only if you used not default ClientConfig + readTimeout: "${REDIS_CLIENT_READ_TIMEOUT:60000}" + # this value may be used only if you used not default ClientConfig + usePoolConfig: "${REDIS_CLIENT_USE_POOL_CONFIG:false}" + cluster: + # Comma-separated list of "host:port" pairs to bootstrap from. + nodes: "${REDIS_NODES:}" + # Maximum number of redirects to follow when executing commands across the cluster. + max-redirects: "${REDIS_MAX_REDIRECTS:12}" + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + # db index + db: "${REDIS_DB:0}" + # db password + password: "${REDIS_PASSWORD:}" + # pool config + pool_config: + maxTotal: "${REDIS_POOL_CONFIG_MAX_TOTAL:128}" + maxIdle: "${REDIS_POOL_CONFIG_MAX_IDLE:128}" + minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" + minEvictableMs: "${REDIS_POOL_CONFIG_MIN_EVICTABLE_MS:60000}" + evictionRunsMs: "${REDIS_POOL_CONFIG_EVICTION_RUNS_MS:30000}" + maxWaitMills: "${REDIS_POOL_CONFIG_MAX_WAIT_MS:60000}" + numberTestsPerEvictionRun: "${REDIS_POOL_CONFIG_NUMBER_TESTS_PER_EVICTION_RUN:3}" + blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" # Check new version updates parameters updates: @@ -320,6 +376,7 @@ spring: repositories: enabled: "true" jpa: + open-in-view: "false" hibernate: ddl-auto: "none" database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}" @@ -371,6 +428,7 @@ audit-log: state: defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:10}" defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:10}" + persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}" kafka: enabled: true @@ -403,7 +461,7 @@ js: # Specify thread pool size for JavaScript sandbox resource monitor monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" # Maximum CPU time in milliseconds allowed for script execution - max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:100}" + max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:3000}" # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${LOCAL_JS_SANDBOX_MAX_ERRORS:3}" # Remote JavaScript environment properties @@ -422,6 +480,9 @@ js: response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}" # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}" + stats: + enabled: "${TB_JS_REMOTE_STATS_ENABLED:false}" + print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}" transport: type: "${TRANSPORT_TYPE:local}" # local or remote @@ -439,6 +500,9 @@ transport: poll_records_pack_size: "${TB_RULE_ENGINE_MAX_POLL_RECORDS:1000}" max_poll_records_per_second: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_SECOND:10000}" max_poll_records_per_minute: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_MINUTE:120000}" + stats: + enabled: "${TB_RULE_ENGINE_STATS_ENABLED:false}" + print_interval_ms: "${TB_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:10000}" notifications: topic: "${TB_TRANSPORT_NOTIFICATIONS_TOPIC:tb.transport.notifications}" sessions: @@ -505,4 +569,4 @@ swagger: 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 + version: "${SWAGGER_VERSION:2.0}" diff --git a/application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java b/application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java new file mode 100644 index 0000000000..ceff438b41 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java @@ -0,0 +1,105 @@ +/** + * 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.cluster.routing; + +import com.datastax.driver.core.utils.UUIDs; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.msg.cluster.ServerAddress; +import org.thingsboard.server.common.msg.cluster.ServerType; +import org.thingsboard.server.service.cluster.discovery.DiscoveryService; +import org.thingsboard.server.service.cluster.discovery.ServerInstance; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(MockitoJUnitRunner.class) +public class ConsistentClusterRoutingServiceTest { + + private ConsistentClusterRoutingService clusterRoutingService; + + private DiscoveryService discoveryService; + + private String hashFunctionName = "murmur3_128"; + private Integer virtualNodesSize = 1024*4; + private ServerAddress currentServer = new ServerAddress(" 100.96.1.0", 9001, ServerType.CORE); + + + @Before + public void setup() throws Exception { + discoveryService = mock(DiscoveryService.class); + clusterRoutingService = new ConsistentClusterRoutingService(); + ReflectionTestUtils.setField(clusterRoutingService, "discoveryService", discoveryService); + ReflectionTestUtils.setField(clusterRoutingService, "hashFunctionName", hashFunctionName); + ReflectionTestUtils.setField(clusterRoutingService, "virtualNodesSize", virtualNodesSize); + when(discoveryService.getCurrentServer()).thenReturn(new ServerInstance(currentServer)); + List otherServers = new ArrayList<>(); + for (int i = 1; i < 30; i++) { + otherServers.add(new ServerInstance(new ServerAddress(" 100.96." + i*2 + "." + i, 9001, ServerType.CORE))); + } + when(discoveryService.getOtherServers()).thenReturn(otherServers); + clusterRoutingService.init(); + } + + @Test + public void testDispersionOnMillionDevices() { + List devices = new ArrayList<>(); + for (int i = 0; i < 1000000; i++) { + devices.add(new DeviceId(UUIDs.timeBased())); + } + + testDevicesDispersion(devices); + } + + private void testDevicesDispersion(List devices) { + long start = System.currentTimeMillis(); + Map map = new HashMap<>(); + for (DeviceId deviceId : devices) { + ServerAddress address = clusterRoutingService.resolveById(deviceId).orElse(currentServer); + map.put(address, map.getOrDefault(address, 0) + 1); + } + + List> data = map.entrySet().stream().sorted(Comparator.comparingInt(Map.Entry::getValue)).collect(Collectors.toList()); + long end = System.currentTimeMillis(); + System.out.println("Size: " + virtualNodesSize + " Time: " + (end - start) + " Diff: " + (data.get(data.size() - 1).getValue() - data.get(0).getValue())); + + for (Map.Entry entry : data) { +// System.out.println(entry.getKey().getHost() + ": " + entry.getValue()); + } + + } + +} diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index b2a8cd467e..57cee90210 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 common org.thingsboard.common diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java similarity index 90% rename from dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java index eb8c800d1e..a5f2dbcab1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java @@ -23,12 +23,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.device.claim.ClaimResult; import java.util.List; +import java.util.concurrent.ExecutionException; 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) throws ExecutionException, InterruptedException; ListenableFuture> reClaimDevice(TenantId tenantId, Device device); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java similarity index 100% rename from dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java diff --git a/common/data/pom.xml b/common/data/pom.xml index 155a00b7f9..98b89d0d6f 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 common org.thingsboard.common diff --git a/application/src/main/java/org/thingsboard/server/controller/claim/data/ClaimRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/controller/claim/data/ClaimRequest.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java index 6620aa187e..8845edb872 100644 --- a/application/src/main/java/org/thingsboard/server/controller/claim/data/ClaimRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.controller.claim.data; +package org.thingsboard.server.common.data; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java rename to common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java index b4e61f7bc2..19a0341dfe 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.update.model; +package org.thingsboard.server.common.data; import lombok.Data; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 174044de28..c409cd2a37 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -31,6 +31,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements private CustomerId customerId; private String name; private String type; + private String label; public Asset() { super(); @@ -46,6 +47,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.customerId = asset.getCustomerId(); this.name = asset.getName(); this.type = asset.getType(); + this.label = asset.getLabel(); } public TenantId getTenantId() { @@ -81,6 +83,14 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.type = type; } + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + @Override public String getSearchText() { return getName(); @@ -97,6 +107,8 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements builder.append(name); builder.append(", type="); builder.append(type); + builder.append(", label="); + builder.append(label); builder.append(", additionalInfo="); builder.append(getAdditionalInfo()); builder.append(", createdTime="); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java index bfc9176b4b..ee531fd4fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.model; +package org.thingsboard.server.common.data.security.model; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java index 0c50b5f6e0..9718c4ecb2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.model; +package org.thingsboard.server.common.data.security.model; import lombok.Data; diff --git a/common/message/pom.xml b/common/message/pom.xml index 60702c0801..30d4b9d957 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 common org.thingsboard.common diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index be346db040..cbc9b979bf 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -67,7 +67,7 @@ public final class TbMsg implements Serializable { this(id, type, originator, metaData, dataType, data, new TbMsgTransactionData(id, originator), ruleChainId, ruleNodeId, clusterPartition); } - public static ByteBuffer toBytes(TbMsg msg) { + public static byte[] toByteArray(TbMsg msg) { MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder(); builder.setId(msg.getId().toString()); builder.setType(msg.getType()); @@ -101,8 +101,16 @@ public final class TbMsg implements Serializable { builder.setDataType(msg.getDataType().ordinal()); builder.setData(msg.getData()); - byte[] bytes = builder.build().toByteArray(); - return ByteBuffer.wrap(bytes); + return builder.build().toByteArray(); + + } + + public static ByteBuffer toBytes(TbMsg msg) { + return ByteBuffer.wrap(toByteArray(msg)); + } + + public static TbMsg fromBytes(byte[] data) { + return fromBytes(ByteBuffer.wrap(data)); } public static TbMsg fromBytes(ByteBuffer buffer) { @@ -115,8 +123,8 @@ public final class TbMsg implements Serializable { EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); RuleChainId ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB())); RuleNodeId ruleNodeId = null; - if(proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) { - ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); + if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) { + ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); } TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, dataType, proto.getData(), transactionData, ruleChainId, ruleNodeId, proto.getClusterPartition()); diff --git a/common/pom.xml b/common/pom.xml index ce20b753ba..29e2223405 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 7821051137..d17f37ad9d 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 common org.thingsboard.common diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java index bcb6797781..0762e78316 100644 --- a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java @@ -49,9 +49,6 @@ public class TBKafkaProducerTemplate { private final KafkaProducer producer; private final TbKafkaEncoder encoder; - @Builder.Default - private TbKafkaEnricher enricher = ((value, responseTopic, requestId) -> value); - private final TbKafkaPartitioner partitioner; private ConcurrentMap> partitionInfoMap; @Getter @@ -61,7 +58,7 @@ public class TBKafkaProducerTemplate { private final TbKafkaSettings settings; @Builder - private TBKafkaProducerTemplate(TbKafkaSettings settings, TbKafkaEncoder encoder, TbKafkaEnricher enricher, + private TBKafkaProducerTemplate(TbKafkaSettings settings, TbKafkaEncoder encoder, TbKafkaPartitioner partitioner, String defaultTopic, String clientId) { Properties props = settings.toProps(); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); @@ -72,7 +69,6 @@ public class TBKafkaProducerTemplate { this.settings = settings; this.producer = new KafkaProducer<>(props); this.encoder = encoder; - this.enricher = enricher; this.partitioner = partitioner; this.defaultTopic = defaultTopic; } @@ -93,14 +89,6 @@ public class TBKafkaProducerTemplate { } } - T enrich(T value, String responseTopic, UUID requestId) { - if (enricher != null) { - return enricher.enrich(value, responseTopic, requestId); - } else { - return value; - } - } - public Future send(String key, T value, Callback callback) { return send(key, value, null, callback); } diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java index 28c1e2cba2..461866e406 100644 --- a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java @@ -144,11 +144,11 @@ public class TbKafkaRequestTemplate extends AbstractTbKafkaTe tickSize = pendingRequests.size(); if (nextCleanupMs < tickTs) { //cleanup; - pendingRequests.entrySet().forEach(kv -> { - if (kv.getValue().expTime < tickTs) { - ResponseMetaData staleRequest = pendingRequests.remove(kv.getKey()); + pendingRequests.forEach((key, value) -> { + if (value.expTime < tickTs) { + ResponseMetaData staleRequest = pendingRequests.remove(key); if (staleRequest != null) { - log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", kv.getKey(), staleRequest.expTime, tickTs); + log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs); staleRequest.future.setException(new TimeoutException()); } } @@ -189,13 +189,12 @@ public class TbKafkaRequestTemplate extends AbstractTbKafkaTe SettableFuture future = SettableFuture.create(); ResponseMetaData responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future); pendingRequests.putIfAbsent(requestId, responseMetaData); - request = requestTemplate.enrich(request, responseTemplate.getTopic(), requestId); log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, key, responseMetaData.expTime); requestTemplate.send(key, request, headers, (metadata, exception) -> { if (exception != null) { log.trace("[{}] Failed to post the request", requestId, exception); } else { - log.trace("[{}] Posted the request", requestId, metadata); + log.trace("[{}] Posted the request: {}", requestId, metadata); } }); return future; diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java index 24d53a846e..bef8244534 100644 --- a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java @@ -32,9 +32,8 @@ import java.util.Properties; @Component public class TbKafkaSettings { - public static final String REQUEST_ID_HEADER = "requestId"; - public static final String RESPONSE_TOPIC_HEADER = "responseTopic"; - + static final String REQUEST_ID_HEADER = "requestId"; + static final String RESPONSE_TOPIC_HEADER = "responseTopic"; @Value("${kafka.bootstrap.servers}") private String servers; diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 26d478b596..0861ce31e3 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.2-SNAPSHOT + 2.4.2 transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 644df5a7b4..51f8e2f0df 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.2-SNAPSHOT + 2.4.2 transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index ef0ae8eabd..7ae8c35afa 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.2-SNAPSHOT + 2.4.2 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 d92b04e584..aa3757c4d7 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 @@ -409,6 +409,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processDisconnect(ChannelHandlerContext ctx) { ctx.close(); + log.info("[{}] Client disconnected!", sessionId); if (deviceSessionCtx.isConnected()) { transportService.process(sessionInfo, AbstractTransportService.getSessionEventMsg(SessionEvent.CLOSED), null); transportService.deregisterSession(sessionInfo); diff --git a/common/transport/pom.xml b/common/transport/pom.xml index ca36845902..abf152f245 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 common org.thingsboard.common diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 88bc2f64d8..2f8fa6614c 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.2-SNAPSHOT + 2.4.2 transport org.thingsboard.common.transport diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java index ad6baf0258..99e1f9b7df 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java @@ -47,7 +47,7 @@ public class TransportContext { @PostConstruct public void init() { - executor = Executors.newCachedThreadPool(); + executor = Executors.newWorkStealingPool(50); } @PreDestroy diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index 24b1343491..af757c2c27 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -201,10 +201,10 @@ public class JsonConverter { .setBoolV(value.getAsBoolean()).build()); } else if (value.isNumber()) { result.add(buildNumericKeyValueProto(value, valueEntry.getKey())); - } else { + } else if (!value.isJsonNull()) { throw new JsonSyntaxException(CAN_T_PARSE_VALUE + value); } - } else { + } else if (!element.isJsonNull()) { throw new JsonSyntaxException(CAN_T_PARSE_VALUE + element); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java index ec7f491fed..632ce16bc1 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java @@ -320,28 +320,19 @@ public class RemoteTransportService extends AbstractTransportService { send(sessionInfo, toRuleEngineMsg, callback); } - private static class TransportCallbackAdaptor implements Callback { - private final TransportServiceCallback callback; - - TransportCallbackAdaptor(TransportServiceCallback callback) { - this.callback = callback; - } - - @Override - public void onCompletion(RecordMetadata metadata, Exception exception) { - if (exception == null) { - if (callback != null) { - callback.onSuccess(null); - } - } else { - if (callback != null) { - callback.onError(exception); + private void send(SessionInfoProto sessionInfo, ToRuleEngineMsg toRuleEngineMsg, TransportServiceCallback callback) { + ruleEngineProducer.send(getRoutingKey(sessionInfo), toRuleEngineMsg, (metadata, exception) -> { + if (callback != null) { + if (exception == null) { + this.transportCallbackExecutor.submit(() -> { + callback.onSuccess(null); + }); + } else { + this.transportCallbackExecutor.submit(() -> { + callback.onError(exception); + }); } } - } - } - - private void send(SessionInfoProto sessionInfo, ToRuleEngineMsg toRuleEngineMsg, TransportServiceCallback callback) { - ruleEngineProducer.send(getRoutingKey(sessionInfo), toRuleEngineMsg, new TransportCallbackAdaptor(callback)); + }); } } diff --git a/common/util/pom.xml b/common/util/pom.xml index c695ac573e..d3ba8c32ee 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 common org.thingsboard.common diff --git a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java b/common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java rename to common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java index 221915d02c..1f839edb80 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java +++ b/common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.executors; +package org.thingsboard.common.util; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import org.thingsboard.rule.engine.api.ListeningExecutor; +import org.thingsboard.common.util.ListeningExecutor; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java b/common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java similarity index 85% rename from rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java rename to common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java index 99df8f30fd..a12cc269b3 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.api; +package org.thingsboard.common.util; import com.google.common.util.concurrent.ListenableFuture; @@ -24,4 +24,8 @@ public interface ListeningExecutor extends Executor { ListenableFuture executeAsync(Callable task); + default ListenableFuture submit(Callable task) { + return executeAsync(task); + } + } diff --git a/dao/pom.xml b/dao/pom.xml index ba44133fb8..3e5e204419 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard dao @@ -47,6 +47,10 @@ org.thingsboard.common dao-api + + org.thingsboard.common + util + org.slf4j slf4j-api 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 d83b068cbe..8a4279fe7f 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 @@ -52,6 +52,7 @@ import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -325,21 +326,21 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ private AlarmSeverity detectHighestSeverity(List alarms) { if (!alarms.isEmpty()) { List sorted = new ArrayList(alarms); - sorted.sort((p1, p2) -> p1.getSeverity().compareTo(p2.getSeverity())); + sorted.sort(Comparator.comparing(Alarm::getSeverity)); return sorted.get(0).getSeverity(); } else { return null; } } - private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) throws ExecutionException, InterruptedException { + private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) { log.debug("Deleting Alarm relation: {}", alarmRelation); - relationService.deleteRelationAsync(tenantId, alarmRelation).get(); + relationService.deleteRelation(tenantId, alarmRelation); } - private void createRelation(TenantId tenantId, EntityRelation alarmRelation) throws ExecutionException, InterruptedException { + private void createRelation(TenantId tenantId, EntityRelation alarmRelation) { log.debug("Creating Alarm relation: {}", alarmRelation); - relationService.saveRelationAsync(tenantId, alarmRelation).get(); + relationService.saveRelation(tenantId, alarmRelation); } private Alarm merge(Alarm existing, Alarm alarm) { @@ -376,28 +377,18 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } private void createAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status, boolean createAnyRelation) { - try { - if (createAnyRelation) { - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM)); - } - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM)); - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM)); - createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM)); - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to create relation. Status: [{}]", alarmId, status); - throw new RuntimeException(e); + if (createAnyRelation) { + createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM)); } + createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM)); + createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM)); + createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM)); } private void deleteAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status) { - try { - deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM)); - deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM)); - deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM)); - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to delete relation. Status: [{}]", alarmId, status); - throw new RuntimeException(e); - } + deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM)); + deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM)); + deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM)); } private void updateAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus oldStatus, AlarmStatus newStatus) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index cf679a103f..8cd3c476f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -20,6 +20,7 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -28,6 +29,7 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; @@ -113,7 +115,22 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ public Asset saveAsset(Asset asset) { log.trace("Executing saveAsset [{}]", asset); assetValidator.validate(asset, Asset::getTenantId); - return assetDao.save(asset.getTenantId(), asset); + Asset savedAsset; + if (!sqlDatabaseUsed) { + savedAsset = assetDao.save(asset.getTenantId(), asset); + } else { + try { + savedAsset = assetDao.save(asset.getTenantId(), asset); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("asset_name_unq_key")) { + throw new DataValidationException("Asset with such name already exists!"); + } else { + throw t; + } + } + } + return savedAsset; } @Override @@ -265,22 +282,26 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Override protected void validateCreate(TenantId tenantId, Asset asset) { - assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( - d -> { - throw new DataValidationException("Asset with such name already exists!"); - } - ); + if (!sqlDatabaseUsed) { + assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( + d -> { + throw new DataValidationException("Asset with such name already exists!"); + } + ); + } } @Override protected void validateUpdate(TenantId tenantId, Asset asset) { - assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( - d -> { - if (!d.getId().equals(asset.getId())) { - throw new DataValidationException("Asset with such name already exists!"); + if (!sqlDatabaseUsed) { + assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( + d -> { + if (!d.getId().equals(asset.getId())) { + throw new DataValidationException("Asset with such name already exists!"); + } } - } - ); + ); + } } @Override 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 f1c4b5a8fc..5ed0222d70 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 @@ -28,38 +28,58 @@ 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.format.support.DefaultFormattingConversionService; import org.springframework.util.Assert; import org.thingsboard.server.common.data.id.EntityId; +import redis.clients.jedis.JedisPoolConfig; @Configuration @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false) @EnableCaching @Data -public class TBRedisCacheConfiguration { +public abstract class TBRedisCacheConfiguration { - @Value("${redis.connection.host}") - private String host; + @Value("${redis.pool_config.maxTotal}") + private int maxTotal; - @Value("${redis.connection.port}") - private Integer port; + @Value("${redis.pool_config.maxIdle}") + private int maxIdle; - @Value("${redis.connection.db}") - private Integer db; + @Value("${redis.pool_config.minIdle}") + private int minIdle; - @Value("${redis.connection.password}") - private String password; + @Value("${redis.pool_config.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${redis.pool_config.testOnReturn}") + private boolean testOnReturn; + + @Value("${redis.pool_config.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${redis.pool_config.minEvictableMs}") + private long minEvictableMs; + + @Value("${redis.pool_config.evictionRunsMs}") + private long evictionRunsMs; + + @Value("${redis.pool_config.maxWaitMills}") + private long maxWaitMills; + + @Value("${redis.pool_config.numberTestsPerEvictionRun}") + private int numberTestsPerEvictionRun; + + @Value("${redis.pool_config.blockWhenExhausted}") + private boolean blockWhenExhausted; @Bean public RedisConnectionFactory redisConnectionFactory() { - JedisConnectionFactory factory = new JedisConnectionFactory(); - factory.setHostName(host); - factory.setPort(port); - factory.setDatabase(db); - factory.setPassword(password); - return factory; + return loadFactory(); } + protected abstract JedisConnectionFactory loadFactory(); + @Bean public CacheManager cacheManager(RedisConnectionFactory cf) { DefaultFormattingConversionService redisConversionService = new DefaultFormattingConversionService(); @@ -74,8 +94,31 @@ public class TBRedisCacheConfiguration { return new PreviousDeviceCredentialsIdKeyGenerator(); } + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + return template; + } + private static void registerDefaultConverters(ConverterRegistry registry) { Assert.notNull(registry, "ConverterRegistry must not be null!"); registry.addConverter(EntityId.class, String.class, EntityId::toString); } + + protected JedisPoolConfig buildPoolConfig() { + final JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(maxTotal); + poolConfig.setMaxIdle(maxIdle); + poolConfig.setMinIdle(minIdle); + poolConfig.setTestOnBorrow(testOnBorrow); + poolConfig.setTestOnReturn(testOnReturn); + poolConfig.setTestWhileIdle(testWhileIdle); + poolConfig.setMinEvictableIdleTimeMillis(minEvictableMs); + poolConfig.setTimeBetweenEvictionRunsMillis(evictionRunsMs); + poolConfig.setMaxWaitMillis(maxWaitMills); + poolConfig.setNumTestsPerEvictionRun(numberTestsPerEvictionRun); + poolConfig.setBlockWhenExhausted(blockWhenExhausted); + return poolConfig; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java new file mode 100644 index 0000000000..8405db3c7d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java @@ -0,0 +1,77 @@ +/** + * 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.cache; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Configuration +@ConditionalOnMissingBean(CaffeineCacheConfiguration.class) +@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster") +public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { + + private static final String COMMA = ","; + private static final String COLON = ":"; + + @Value("${redis.cluster.nodes}") + private String clusterNodes; + + @Value("${redis.cluster.max-redirects}") + private Integer maxRedirects; + + @Value("${redis.cluster.useDefaultPoolConfig}") + private boolean useDefaultPoolConfig; + + @Value("${redis.password}") + private String password; + + public JedisConnectionFactory loadFactory() { + RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(); + clusterConfiguration.setClusterNodes(getNodes(clusterNodes)); + clusterConfiguration.setMaxRedirects(maxRedirects); + clusterConfiguration.setPassword(password); + if (useDefaultPoolConfig) { + return new JedisConnectionFactory(clusterConfiguration); + } else { + return new JedisConnectionFactory(clusterConfiguration, buildPoolConfig()); + } + } + + private List getNodes(String nodes) { + List result; + if (StringUtils.isBlank(nodes)) { + result = Collections.emptyList(); + } else { + result = new ArrayList<>(); + for (String hostPort : nodes.split(COMMA)) { + String host = hostPort.split(COLON)[0]; + Integer port = Integer.valueOf(hostPort.split(COLON)[1]); + result.add(new RedisNode(host, port)); + } + } + return result; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java new file mode 100644 index 0000000000..b09b0dd565 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java @@ -0,0 +1,88 @@ +/** + * 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.cache; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + +import java.time.Duration; + +@Configuration +@ConditionalOnMissingBean(CaffeineCacheConfiguration.class) +@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone") +public class TBRedisStandaloneConfiguration extends TBRedisCacheConfiguration { + + @Value("${redis.standalone.host}") + private String host; + + @Value("${redis.standalone.port}") + private Integer port; + + @Value("${redis.standalone.clientName}") + private String clientName; + + @Value("${redis.standalone.connectTimeout}") + private Long connectTimeout; + + @Value("${redis.standalone.readTimeout}") + private Long readTimeout; + + @Value("${redis.standalone.useDefaultClientConfig}") + private boolean useDefaultClientConfig; + + @Value("${redis.standalone.usePoolConfig}") + private boolean usePoolConfig; + + @Value("${redis.db}") + private Integer db; + + @Value("${redis.password}") + private String password; + + public JedisConnectionFactory loadFactory() { + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(); + standaloneConfiguration.setHostName(host); + standaloneConfiguration.setPort(port); + standaloneConfiguration.setDatabase(db); + standaloneConfiguration.setPassword(password); + if (useDefaultClientConfig) { + return new JedisConnectionFactory(standaloneConfiguration); + } else { + return new JedisConnectionFactory(standaloneConfiguration, buildClientConfig()); + } + } + + private JedisClientConfiguration buildClientConfig() { + if (usePoolConfig) { + return JedisClientConfiguration.builder() + .clientName(clientName) + .connectTimeout(Duration.ofMillis(connectTimeout)) + .readTimeout(Duration.ofMillis(readTimeout)) + .usePooling().poolConfig(buildPoolConfig()) + .build(); + } else { + return JedisClientConfiguration.builder() + .clientName(clientName) + .connectTimeout(Duration.ofMillis(connectTimeout)) + .readTimeout(Duration.ofMillis(readTimeout)).build(); + } + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index b12e114d93..3886a9de65 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -54,11 +54,7 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic public ComponentDescriptor saveComponent(TenantId tenantId, ComponentDescriptor component) { componentValidator.validate(component, data -> new TenantId(EntityId.NULL_UUID)); Optional result = componentDescriptorDao.saveIfNotExist(tenantId, component); - if (result.isPresent()) { - return result.get(); - } else { - return componentDescriptorDao.findByClazz(tenantId, component.getClazz()); - } + return result.orElseGet(() -> componentDescriptorDao.findByClazz(tenantId, component.getClazz())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.java b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.java new file mode 100644 index 0000000000..3b3194dbbf --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.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; + +import lombok.Data; +import org.thingsboard.server.dao.device.claim.ClaimData; + +import java.util.List; + +@Data +public class ClaimDataInfo { + + private final boolean fromCache; + private final List key; + private final ClaimData data; + +} 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 9000c91823..6955cd761c 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.device; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; @@ -23,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; @@ -37,9 +39,12 @@ 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.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutionException; import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE; @@ -48,6 +53,8 @@ import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CA public class ClaimDevicesServiceImpl implements ClaimDevicesService { private static final String CLAIM_ATTRIBUTE_NAME = "claimingAllowed"; + private static final String CLAIM_DATA_ATTRIBUTE_NAME = "claimingData"; + private static final ObjectMapper mapper = new ObjectMapper(); @Autowired private DeviceService deviceService; @@ -95,24 +102,45 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { }); } - @Override - public ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) { + private ClaimDataInfo getClaimData(Cache cache, Device device) throws ExecutionException, InterruptedException { List key = constructCacheKey(device.getId()); + ClaimData claimDataFromCache = cache.get(key, ClaimData.class); + if (claimDataFromCache != null) { + return new ClaimDataInfo(true, key, claimDataFromCache); + } else { + Optional claimDataAttr = attributesService.find(device.getTenantId(), device.getId(), + DataConstants.SERVER_SCOPE, CLAIM_DATA_ATTRIBUTE_NAME).get(); + if (claimDataAttr.isPresent()) { + try { + ClaimData claimDataFromAttribute = mapper.readValue(claimDataAttr.get().getValueAsString(), ClaimData.class); + return new ClaimDataInfo(false, key, claimDataFromAttribute); + } catch (IOException e) { + log.warn("Failed to read Claim Data [{}] from attribute!", claimDataAttr, e); + } + } + } + return null; + } + + @Override + public ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) throws ExecutionException, InterruptedException { Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); - ClaimData claimData = cache.get(key, ClaimData.class); + ClaimDataInfo claimData = getClaimData(cache, device); if (claimData != null) { long currTs = System.currentTimeMillis(); - if (currTs > claimData.getExpirationTime() || !secretKey.equals(claimData.getSecretKey())) { + if (currTs > claimData.getData().getExpirationTime() || !secretKeyIsEmptyOrEqual(secretKey, claimData.getData().getSecretKey())) { log.warn("The claiming timeout occurred or wrong 'secretKey' provided for the device [{}]", device.getName()); - cache.evict(key); + if (claimData.isFromCache()) { + cache.evict(claimData.getKey()); + } return Futures.immediateFuture(new ClaimResult(null, ClaimResponse.FAILURE)); } else { if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { device.setCustomerId(customerId); Device savedDevice = deviceService.saveDevice(device); - return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(savedDevice, ClaimResponse.SUCCESS)); + return Futures.transform(removeClaimingSavedData(cache, claimData, device), result -> new ClaimResult(savedDevice, ClaimResponse.SUCCESS)); } - return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(null, ClaimResponse.CLAIMED)); + return Futures.transform(removeClaimingSavedData(cache, claimData, device), result -> new ClaimResult(null, ClaimResponse.CLAIMED)); } } else { log.warn("Failed to find the device's claiming message![{}]", device.getName()); @@ -124,6 +152,10 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { } } + private boolean secretKeyIsEmptyOrEqual(String secretKeyA, String secretKeyB) { + return (StringUtils.isEmpty(secretKeyA) && StringUtils.isEmpty(secretKeyB)) || secretKeyA.equals(secretKeyB); + } + @Override public ListenableFuture> reClaimDevice(TenantId tenantId, Device device) { if (!device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { @@ -159,13 +191,12 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { return systemDurationMs; } - private ListenableFuture> removeClaimingSavedData(Cache cache, List key, Device device) { - cache.evict(key); - if (isAllowedClaimingByDefault) { - return Futures.immediateFuture(null); + private ListenableFuture> removeClaimingSavedData(Cache cache, ClaimDataInfo data, Device device) { + if (data.isFromCache()) { + cache.evict(data.getKey()); } return attributesService.removeAll(device.getTenantId(), - device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); + device.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(CLAIM_ATTRIBUTE_NAME, CLAIM_DATA_ATTRIBUTE_NAME)); } private void cacheEviction(DeviceId deviceId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 7c7910ba8f..c14d25b65b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device; import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -38,7 +40,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Service @Slf4j -public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { +public class DeviceCredentialsServiceImpl extends AbstractEntityService implements DeviceCredentialsService { @Autowired private DeviceCredentialsDao deviceCredentialsDao; @@ -78,7 +80,20 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { } log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials, id -> tenantId); - return deviceCredentialsDao.save(tenantId, deviceCredentials); + if (!sqlDatabaseUsed) { + return deviceCredentialsDao.save(tenantId, deviceCredentials); + } else { + try { + return deviceCredentialsDao.save(tenantId, deviceCredentials); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_credentials_id_unq_key")) { + throw new DataValidationException("Specified credentials are already registered!"); + } else { + throw t; + } + } + } } private void formatCertData(DeviceCredentials deviceCredentials) { @@ -100,9 +115,11 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { @Override protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) { - DeviceCredentials existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); - if (existingCredentialsEntity != null) { - throw new DataValidationException("Create of existent device credentials!"); + if (!sqlDatabaseUsed) { + DeviceCredentials existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); + if (existingCredentialsEntity != null) { + throw new DataValidationException("Create of existent device credentials!"); + } } } @@ -112,9 +129,11 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { if (existingCredentials == null) { throw new DataValidationException("Unable to update non-existent device credentials!"); } - DeviceCredentials sameCredentialsId = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); - if (sameCredentialsId != null && !sameCredentialsId.getUuidId().equals(deviceCredentials.getUuidId())) { - throw new DataValidationException("Specified credentials are already registered!"); + if (!sqlDatabaseUsed) { + DeviceCredentials sameCredentialsId = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); + if (sameCredentialsId != null && !sameCredentialsId.getUuidId().equals(deviceCredentials.getUuidId())) { + throw new DataValidationException("Specified credentials are already registered!"); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 872e03a387..a26e275a96 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -123,7 +124,21 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe public Device saveDevice(Device device) { log.trace("Executing saveDevice [{}]", device); deviceValidator.validate(device, Device::getTenantId); - Device savedDevice = deviceDao.save(device.getTenantId(), device); + Device savedDevice; + if (!sqlDatabaseUsed) { + savedDevice = deviceDao.save(device.getTenantId(), device); + } else { + try { + savedDevice = deviceDao.save(device.getTenantId(), device); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { + throw new DataValidationException("Device with such name already exists!"); + } else { + throw t; + } + } + } if (device.getId() == null) { DeviceCredentials deviceCredentials = new DeviceCredentials(); deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); @@ -296,22 +311,26 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Override protected void validateCreate(TenantId tenantId, Device device) { - deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent( - d -> { - throw new DataValidationException("Device with such name already exists!"); - } - ); + if (!sqlDatabaseUsed) { + deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent( + d -> { + throw new DataValidationException("Device with such name already exists!"); + } + ); + } } @Override protected void validateUpdate(TenantId tenantId, Device device) { - deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent( - d -> { - if (!d.getUuidId().equals(device.getUuidId())) { - throw new DataValidationException("Device with such name already exists!"); + if (!sqlDatabaseUsed) { + deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent( + d -> { + if (!d.getUuidId().equals(device.getUuidId())) { + throw new DataValidationException("Device with such name already exists!"); + } } - } - ); + ); + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 5280637020..2fce1fd758 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -16,20 +16,45 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +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.dao.relation.RelationService; +import javax.annotation.PostConstruct; +import java.util.Optional; + @Slf4j public abstract class AbstractEntityService { @Autowired protected RelationService relationService; + @Value("${database.entities.type:sql}") + private String databaseType; + + protected boolean sqlDatabaseUsed; + + @PostConstruct + public void init() { + sqlDatabaseUsed = "sql".equalsIgnoreCase(databaseType); + } + protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); relationService.deleteEntityRelations(tenantId, entityId); } + protected Optional extractConstraintViolationException(Exception t) { + if (t instanceof ConstraintViolationException) { + return Optional.of ((ConstraintViolationException) t); + } else if (t.getCause() instanceof ConstraintViolationException) { + return Optional.of ((ConstraintViolationException) (t.getCause())); + } else { + return Optional.empty(); + } + } + } 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 f065443489..c466c034fc 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 @@ -197,6 +197,7 @@ public class ModelConstants { public static final String ASSET_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String ASSET_NAME_PROPERTY = "name"; public static final String ASSET_TYPE_PROPERTY = "type"; + public static final String ASSET_LABEL_PROPERTY = "label"; public static final String ASSET_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_and_search_text"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java index a951b970f3..8313b44afe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java @@ -37,6 +37,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; @@ -64,6 +65,9 @@ public final class AssetEntity implements SearchTextEntity { @Column(name = ASSET_NAME_PROPERTY) private String name; + @Column(name = ASSET_LABEL_PROPERTY) + private String label; + @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; @@ -86,6 +90,7 @@ public final class AssetEntity implements SearchTextEntity { } this.name = asset.getName(); this.type = asset.getType(); + this.label = asset.getLabel(); this.additionalInfo = asset.getAdditionalInfo(); } @@ -163,8 +168,9 @@ public final class AssetEntity implements SearchTextEntity { } asset.setName(name); asset.setType(type); + asset.setLabel(label); asset.setAdditionalInfo(additionalInfo); return asset; } -} \ No newline at end of file +} 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/AbstractTsKvEntity.java similarity index 85% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java index 2773e5e4ab..4c8a2606d8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java @@ -16,14 +16,11 @@ 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; @@ -35,11 +32,10 @@ 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 { +public abstract class AbstractTsKvEntity { protected static final String SUM = "SUM"; protected static final String AVG = "AVG"; @@ -50,10 +46,6 @@ public abstract class AbsractTsKvEntity implements ToData { @Column(name = ENTITY_ID_COLUMN) protected String entityId; - @Id - @Column(name = TS_COLUMN) - protected Long ts; - @Id @Column(name = KEY_COLUMN) protected String key; @@ -70,8 +62,7 @@ public abstract class AbsractTsKvEntity implements ToData { @Column(name = DOUBLE_VALUE_COLUMN) protected Double doubleValue; - @Override - public TsKvEntry toData() { + protected KvEntry getKvEntry() { KvEntry kvEntry = null; if (strValue != null) { kvEntry = new StringDataEntry(key, strValue); @@ -82,14 +73,14 @@ public abstract class AbsractTsKvEntity implements ToData { } else if (booleanValue != null) { kvEntry = new BooleanDataEntry(key, booleanValue); } - return new BasicTsKvEntry(ts, kvEntry); + return kvEntry; } public abstract boolean isNotEmpty(); protected static boolean isAllNull(Object... args) { for (Object arg : args) { - if(arg != null) { + if (arg != null) { return false; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java index 043a02b813..bfc8020a7b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java @@ -40,6 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; @Data @@ -61,6 +62,9 @@ public final class AssetEntity extends BaseSqlEntity implements SearchTex @Column(name = ASSET_TYPE_PROPERTY) private String type; + @Column(name = ASSET_LABEL_PROPERTY) + private String label; + @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; @@ -84,6 +88,7 @@ public final class AssetEntity extends BaseSqlEntity implements SearchTex } this.name = asset.getName(); this.type = asset.getType(); + this.label = asset.getLabel(); this.additionalInfo = asset.getAdditionalInfo(); } @@ -113,8 +118,9 @@ public final class AssetEntity extends BaseSqlEntity implements SearchTex } asset.setName(name); asset.setType(type); + asset.setLabel(label); asset.setAdditionalInfo(additionalInfo); return asset; } -} \ 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 index fa212dd2f9..753e2c10fa 100644 --- 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 @@ -18,9 +18,10 @@ 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.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.model.ToData; -import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import javax.persistence.Column; import javax.persistence.ColumnResult; @@ -35,6 +36,7 @@ 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.model.ModelConstants.TS_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; @@ -113,12 +115,16 @@ import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.F resultSetMapping = "timescaleCountMapping" ) }) -public final class TimescaleTsKvEntity extends AbsractTsKvEntity implements ToData { +public final class TimescaleTsKvEntity extends AbstractTsKvEntity implements ToData { @Id @Column(name = TENANT_ID_COLUMN) private String tenantId; + @Id + @Column(name = TS_COLUMN) + protected Long ts; + public TimescaleTsKvEntity() { } public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType) { @@ -181,4 +187,9 @@ public final class TimescaleTsKvEntity extends AbsractTsKvEntity implements ToDa public boolean isNotEmpty() { return ts != null && (strValue != null || longValue != null || doubleValue != null || booleanValue != null); } + + @Override + public TsKvEntry toData() { + return new BasicTsKvEntry(ts, getKvEntry()); + } } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java index 4440a3dd0c..dab344cb44 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java @@ -17,9 +17,10 @@ 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.TsKvEntry; import org.thingsboard.server.dao.model.ToData; -import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import javax.persistence.Column; import javax.persistence.Entity; @@ -30,18 +31,23 @@ import javax.persistence.IdClass; import javax.persistence.Table; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; @Data @Entity @Table(name = "ts_kv") @IdClass(TsKvCompositeKey.class) -public final class TsKvEntity extends AbsractTsKvEntity implements ToData { +public final class TsKvEntity extends AbstractTsKvEntity implements ToData { @Id @Enumerated(EnumType.STRING) @Column(name = ENTITY_TYPE_COLUMN) private EntityType entityType; + @Id + @Column(name = TS_COLUMN) + protected Long ts; + public TsKvEntity() { } @@ -100,9 +106,13 @@ public final class TsKvEntity extends AbsractTsKvEntity implements ToData { +public final class TsKvLatestEntity extends AbstractTsKvEntity implements ToData { - - //TODO: reafctor this and TsKvEntity to avoid code duplicates @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; - @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); + return new BasicTsKvEntry(ts, getKvEntry()); + } + + @Override + public boolean isNotEmpty() { + return strValue != null || longValue != null || doubleValue != null || booleanValue != null; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java index 12c2ee85f5..7870135592 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java @@ -56,19 +56,32 @@ public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor< @Value("${cassandra.query.poll_ms:50}") long pollMs, @Value("${cassandra.query.tenant_rate_limits.enabled}") boolean tenantRateLimitsEnabled, @Value("${cassandra.query.tenant_rate_limits.configuration}") String tenantRateLimitsConfiguration, - @Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames) { - super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration); + @Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames, + @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq) { + super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration, printQueriesFreq); this.printTenantNames = printTenantNames; } @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") public void printStats() { - log.info("Permits queueSize [{}] totalAdded [{}] totalLaunched [{}] totalReleased [{}] totalFailed [{}] totalExpired [{}] totalRejected [{}] " + - "totalRateLimited [{}] totalRateLimitedTenants [{}] currBuffer [{}] ", - getQueueSize(), - totalAdded.getAndSet(0), totalLaunched.getAndSet(0), totalReleased.getAndSet(0), - totalFailed.getAndSet(0), totalExpired.getAndSet(0), totalRejected.getAndSet(0), - totalRateLimited.getAndSet(0), rateLimitedTenants.size(), concurrencyLevel.get()); + int queueSize = getQueueSize(); + int totalAddedValue = totalAdded.getAndSet(0); + int totalLaunchedValue = totalLaunched.getAndSet(0); + int totalReleasedValue = totalReleased.getAndSet(0); + int totalFailedValue = totalFailed.getAndSet(0); + int totalExpiredValue = totalExpired.getAndSet(0); + int totalRejectedValue = totalRejected.getAndSet(0); + int totalRateLimitedValue = totalRateLimited.getAndSet(0); + int rateLimitedTenantsValue = rateLimitedTenants.size(); + int concurrencyLevelValue = concurrencyLevel.get(); + if (queueSize > 0 || totalAddedValue > 0 || totalLaunchedValue > 0 || totalReleasedValue > 0 || + totalFailedValue > 0 || totalExpiredValue > 0 || totalRejectedValue > 0 || totalRateLimitedValue > 0 || rateLimitedTenantsValue > 0 + || concurrencyLevelValue > 0) { + log.info("Permits queueSize [{}] totalAdded [{}] totalLaunched [{}] totalReleased [{}] totalFailed [{}] totalExpired [{}] totalRejected [{}] " + + "totalRateLimited [{}] totalRateLimitedTenants [{}] currBuffer [{}] ", + queueSize, totalAddedValue, totalLaunchedValue, totalReleasedValue, + totalFailedValue, totalExpiredValue, totalRejectedValue, totalRateLimitedValue, rateLimitedTenantsValue, concurrencyLevelValue); + } rateLimitedTenants.forEach(((tenantId, counter) -> { if (printTenantNames) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java index 16368dff71..16723c024d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java @@ -17,16 +17,14 @@ package org.thingsboard.server.dao.sql; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.PreDestroy; import java.util.concurrent.Executors; public abstract class JpaAbstractDaoListeningExecutorService { - protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); + @Autowired + protected JpaExecutorService service; - @PreDestroy - void onDestroy() { - service.shutdown(); - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java new file mode 100644 index 0000000000..998b8bb444 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.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.sql; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; +import org.thingsboard.server.dao.util.SqlDao; + +@Component +@SqlDao +public class JpaExecutorService extends AbstractListeningExecutor { + + @Value("${spring.datasource.hikari.maximumPoolSize}") + private int poolSize; + + @Override + protected int getThreadPollSize() { + return poolSize; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java new file mode 100644 index 0000000000..da20ff8c21 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql; + +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Component +public class ScheduledLogExecutorComponent { + + private ScheduledExecutorService schedulerLogExecutor; + + @PostConstruct + public void init() { + schedulerLogExecutor = Executors.newSingleThreadScheduledExecutor(); + } + + @PreDestroy + public void stop() { + if (schedulerLogExecutor != null) { + schedulerLogExecutor.shutdownNow(); + } + } + + public void scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + schedulerLogExecutor.scheduleAtFixedRate(command, initialDelay, period, unit); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java new file mode 100644 index 0000000000..da0a9cd70d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -0,0 +1,116 @@ +/** + * 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; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@Slf4j +public class TbSqlBlockingQueue implements TbSqlQueue { + + private final BlockingQueue> queue = new LinkedBlockingQueue<>(); + private final AtomicInteger addedCount = new AtomicInteger(); + private final AtomicInteger savedCount = new AtomicInteger(); + private final AtomicInteger failedCount = new AtomicInteger(); + private final TbSqlBlockingQueueParams params; + + private ExecutorService executor; + private ScheduledLogExecutorComponent logExecutor; + + public TbSqlBlockingQueue(TbSqlBlockingQueueParams params) { + this.params = params; + } + + @Override + public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction) { + this.logExecutor = logExecutor; + executor = Executors.newSingleThreadExecutor(); + executor.submit(() -> { + String logName = params.getLogName(); + int batchSize = params.getBatchSize(); + long maxDelay = params.getMaxDelay(); + List> entities = new ArrayList<>(batchSize); + while (!Thread.interrupted()) { + try { + long currentTs = System.currentTimeMillis(); + TbSqlQueueElement attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS); + if (attr == null) { + continue; + } else { + entities.add(attr); + } + queue.drainTo(entities, batchSize - 1); + boolean fullPack = entities.size() == batchSize; + log.debug("[{}] Going to save {} entities", logName, entities.size()); + saveFunction.accept(entities.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList())); + entities.forEach(v -> v.getFuture().set(null)); + savedCount.addAndGet(entities.size()); + if (!fullPack) { + long remainingDelay = maxDelay - (System.currentTimeMillis() - currentTs); + if (remainingDelay > 0) { + Thread.sleep(remainingDelay); + } + } + } catch (Exception e) { + failedCount.addAndGet(entities.size()); + entities.forEach(entityFutureWrapper -> entityFutureWrapper.getFuture().setException(e)); + if (e instanceof InterruptedException) { + log.info("[{}] Queue polling was interrupted", logName); + break; + } else { + log.error("[{}] Failed to save {} entities", logName, entities.size(), e); + } + } finally { + entities.clear(); + } + } + }); + + logExecutor.scheduleAtFixedRate(() -> { + if (queue.size() > 0 || addedCount.get() > 0 || savedCount.get() > 0 || failedCount.get() > 0) { + log.info("[{}] queueSize [{}] totalAdded [{}] totalSaved [{}] totalFailed [{}]", + params.getLogName(), queue.size(), addedCount.getAndSet(0), savedCount.getAndSet(0), failedCount.getAndSet(0)); + } + }, params.getStatsPrintIntervalMs(), params.getStatsPrintIntervalMs(), TimeUnit.MILLISECONDS); + } + + @Override + public void destroy() { + if (executor != null) { + executor.shutdownNow(); + } + } + + @Override + public ListenableFuture add(E element) { + SettableFuture future = SettableFuture.create(); + queue.add(new TbSqlQueueElement<>(future, element)); + addedCount.incrementAndGet(); + return future; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java new file mode 100644 index 0000000000..9afe19a61a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.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.dao.sql; + +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +@Builder +public class TbSqlBlockingQueueParams { + + private final String logName; + private final int batchSize; + private final long maxDelay; + private final long statsPrintIntervalMs; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java new file mode 100644 index 0000000000..27c6bc9509 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.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.sql; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.function.Consumer; + +public interface TbSqlQueue { + + void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction); + + void destroy(); + + ListenableFuture add(E element); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java new file mode 100644 index 0000000000..7c95d768e7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.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.dao.sql; + +import com.google.common.util.concurrent.SettableFuture; +import lombok.Getter; + +public final class TbSqlQueueElement { + @Getter + private final SettableFuture future; + @Getter + private final E entity; + + public TbSqlQueueElement(SettableFuture future, E entity) { + this.future = future; + this.entity = entity; + } +} + + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index a3d5b45a39..756ebbf070 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -31,11 +31,8 @@ import java.util.List; @SqlDao public interface AlarmRepository extends CrudRepository { - @Query("SELECT a FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.originatorId = :originatorId " + - "AND a.originatorType = :entityType AND a.type = :alarmType ORDER BY a.type ASC, a.id DESC") - List findLatestByOriginatorAndType(@Param("tenantId") String tenantId, - @Param("originatorId") String originatorId, - @Param("entityType") EntityType entityType, + @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType ORDER BY startTs DESC") + List findLatestByOriginatorAndType(@Param("originatorId") String originatorId, @Param("alarmType") String alarmType, Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 4762c5c4ec..8aa9a81094 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -77,11 +77,9 @@ public class JpaAlarmDao extends JpaAbstractDao implements A public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return service.submit(() -> { List latest = alarmRepository.findLatestByOriginatorAndType( - UUIDConverter.fromTimeUUID(tenantId.getId()), UUIDConverter.fromTimeUUID(originator.getId()), - originator.getEntityType(), type, - new PageRequest(0, 1)); + PageRequest.of(0, 1)); return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0)); }); } 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 index e50a60aed9..0a537cbe01 100644 --- 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 @@ -15,23 +15,59 @@ */ package org.thingsboard.server.dao.sql.attributes; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.repository.Modifying; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.util.SqlDao; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; @SqlDao @Repository +@Slf4j public abstract class AttributeKvInsertRepository { + private static final ThreadLocal PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE))); + private static final String EMPTY_STR = ""; + + private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, last_update_ts = ? " + + "WHERE entity_type = ? and entity_id = ? and attribute_type =? and attribute_key = ?;"; + + private static final String INSERT_OR_UPDATE = + "INSERT INTO attribute_kv (entity_type, entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, last_update_ts) " + + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT (entity_type, entity_id, attribute_type, attribute_key) " + + "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, last_update_ts = ?;"; + 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"; + @Autowired + protected JdbcTemplate jdbcTemplate; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Value("${sql.remove_null_chars}") + private boolean removeNullChars; + @PersistenceContext protected EntityManager entityManager; @@ -71,7 +107,7 @@ public abstract class AttributeKvInsertRepository { .setParameter("entity_id", entity.getId().getEntityId()) .setParameter("attribute_type", entity.getId().getAttributeType()) .setParameter("attribute_key", entity.getId().getAttributeKey()) - .setParameter("str_v", entity.getStrValue()) + .setParameter("str_v", replaceNullChars(entity.getStrValue())) .setParameter("last_update_ts", entity.getLastUpdateTs()) .executeUpdate(); } @@ -99,4 +135,112 @@ public abstract class AttributeKvInsertRepository { .setParameter("last_update_ts", entity.getLastUpdateTs()) .executeUpdate(); } + + protected void saveOrUpdate(List entities) { + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, replaceNullChars(entities.get(i).getStrValue())); + + if (entities.get(i).getLongValue() != null) { + ps.setLong(2, entities.get(i).getLongValue()); + } else { + ps.setNull(2, Types.BIGINT); + } + + if (entities.get(i).getDoubleValue() != null) { + ps.setDouble(3, entities.get(i).getDoubleValue()); + } else { + ps.setNull(3, Types.DOUBLE); + } + + if (entities.get(i).getBooleanValue() != null) { + ps.setBoolean(4, entities.get(i).getBooleanValue()); + } else { + ps.setNull(4, Types.BOOLEAN); + } + + ps.setLong(5, entities.get(i).getLastUpdateTs()); + ps.setString(6, entities.get(i).getId().getEntityType().name()); + ps.setString(7, entities.get(i).getId().getEntityId()); + ps.setString(8, entities.get(i).getId().getAttributeType()); + ps.setString(9, entities.get(i).getId().getAttributeKey()); + } + + @Override + public int getBatchSize() { + return entities.size(); + } + }); + + int updatedCount = 0; + for (int i = 0; i < result.length; i++) { + if (result[i] == 0) { + updatedCount++; + } + } + + List insertEntities = new ArrayList<>(updatedCount); + for (int i = 0; i < result.length; i++) { + if (result[i] == 0) { + insertEntities.add(entities.get(i)); + } + } + + jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, insertEntities.get(i).getId().getEntityType().name()); + ps.setString(2, insertEntities.get(i).getId().getEntityId()); + ps.setString(3, insertEntities.get(i).getId().getAttributeType()); + ps.setString(4, insertEntities.get(i).getId().getAttributeKey()); + ps.setString(5, replaceNullChars(insertEntities.get(i).getStrValue())); + ps.setString(10, replaceNullChars(insertEntities.get(i).getStrValue())); + + if (insertEntities.get(i).getLongValue() != null) { + ps.setLong(6, insertEntities.get(i).getLongValue()); + ps.setLong(11, insertEntities.get(i).getLongValue()); + } else { + ps.setNull(6, Types.BIGINT); + ps.setNull(11, Types.BIGINT); + } + + if (insertEntities.get(i).getDoubleValue() != null) { + ps.setDouble(7, insertEntities.get(i).getDoubleValue()); + ps.setDouble(12, insertEntities.get(i).getDoubleValue()); + } else { + ps.setNull(7, Types.DOUBLE); + ps.setNull(12, Types.DOUBLE); + } + + if (insertEntities.get(i).getBooleanValue() != null) { + ps.setBoolean(8, insertEntities.get(i).getBooleanValue()); + ps.setBoolean(13, insertEntities.get(i).getBooleanValue()); + } else { + ps.setNull(8, Types.BOOLEAN); + ps.setNull(13, Types.BOOLEAN); + } + + ps.setLong(9, insertEntities.get(i).getLastUpdateTs()); + ps.setLong(14, insertEntities.get(i).getLastUpdateTs()); + } + + @Override + public int getBatchSize() { + return insertEntities.size(); + } + }); + } + }); + } + + private String replaceNullChars(String strValue) { + if (removeNullChars && strValue != null) { + return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR); + } + return strValue; + } } \ 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 index f2343cd7fe..425832f344 100644 --- 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 @@ -21,6 +21,9 @@ import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.util.HsqlDao; import org.thingsboard.server.dao.util.SqlDao; +import java.sql.Types; +import java.util.List; + @SqlDao @HsqlDao @Repository @@ -37,6 +40,17 @@ public class HsqlAttributesInsertRepository extends AttributeKvInsertRepository 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); + private static final String INSERT_OR_UPDATE = + "MERGE INTO attribute_kv USING(VALUES ?, ?, ?, ?, ?, ?, ?, ?, ?) " + + "A (entity_type, entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, 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.str_v = A.str_v, attribute_kv.long_v = A.long_v, attribute_kv.dbl_v = A.dbl_v, attribute_kv.bool_v = A.bool_v, attribute_kv.last_update_ts = A.last_update_ts " + + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, last_update_ts) " + + "VALUES (A.entity_type, A.entity_id, A.attribute_type, A.attribute_key, A.str_v, A.long_v, A.dbl_v, A.bool_v, A.last_update_ts)"; + @Override public void saveOrUpdate(AttributeKvEntity entity) { processSaveOrUpdate(entity, INSERT_BOOL_STATEMENT, INSERT_STR_STATEMENT, INSERT_LONG_STATEMENT, INSERT_DBL_STATEMENT); @@ -45,4 +59,38 @@ public class HsqlAttributesInsertRepository extends AttributeKvInsertRepository 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)"; } + + + @Override + protected void saveOrUpdate(List entities) { + entities.forEach(entity -> { + jdbcTemplate.update(INSERT_OR_UPDATE, ps -> { + ps.setString(1, entity.getId().getEntityType().name()); + ps.setString(2, entity.getId().getEntityId()); + ps.setString(3, entity.getId().getAttributeType()); + ps.setString(4, entity.getId().getAttributeKey()); + ps.setString(5, entity.getStrValue()); + + if (entity.getLongValue() != null) { + ps.setLong(6, entity.getLongValue()); + } else { + ps.setNull(6, Types.BIGINT); + } + + if (entity.getDoubleValue() != null) { + ps.setDouble(7, entity.getDoubleValue()); + } else { + ps.setNull(7, Types.DOUBLE); + } + + if (entity.getBooleanValue() != null) { + ps.setBoolean(8, entity.getBooleanValue()); + } else { + ps.setNull(8, Types.BOOLEAN); + } + + ps.setLong(9, entity.getLastUpdateTs()); + }); + }); + } } \ 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 b8ad6f8271..2b8c7e5ab2 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 @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityId; @@ -30,8 +31,13 @@ import org.thingsboard.server.dao.attributes.AttributesDao; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; +import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueue; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.util.SqlDao; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -44,12 +50,45 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; @SqlDao public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService implements AttributesDao { + @Autowired + ScheduledLogExecutorComponent logExecutor; + @Autowired private AttributeKvRepository attributeKvRepository; @Autowired private AttributeKvInsertRepository attributeKvInsertRepository; + @Value("${sql.attributes.batch_size:1000}") + private int batchSize; + + @Value("${sql.attributes.batch_max_delay:100}") + private long maxDelay; + + @Value("${sql.attributes.stats_print_interval_ms:1000}") + private long statsPrintIntervalMs; + + private TbSqlBlockingQueue queue; + + @PostConstruct + private void init() { + TbSqlBlockingQueueParams params = TbSqlBlockingQueueParams.builder() + .logName("Attributes") + .batchSize(batchSize) + .maxDelay(maxDelay) + .statsPrintIntervalMs(statsPrintIntervalMs) + .build(); + queue = new TbSqlBlockingQueue<>(params); + queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v)); + } + + @PreDestroy + private void destroy() { + if (queue != null) { + queue.destroy(); + } + } + @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey) { AttributeKvCompositeKey compositeKey = @@ -89,12 +128,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl entity.setDoubleValue(attribute.getDoubleValue().orElse(null)); entity.setLongValue(attribute.getLongValue().orElse(null)); entity.setBooleanValue(attribute.getBooleanValue().orElse(null)); - return service.submit(() -> { - attributeKvInsertRepository.saveOrUpdate(entity); - return null; - }); + return addToQueue(entity); } + private ListenableFuture addToQueue(AttributeKvEntity entity) { + return queue.add(entity); + } @Override public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys) { 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 a2c18c541b..9b6638b6cf 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 @@ -53,8 +53,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; @SqlDao public class JpaAuditLogDao extends JpaAbstractDao implements AuditLogDao { - private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); - @Autowired private AuditLogRepository auditLogRepository; @@ -68,14 +66,9 @@ public class JpaAuditLogDao extends JpaAbstractDao imp return auditLogRepository; } - @PreDestroy - void onDestroy() { - insertService.shutdown(); - } - @Override public ListenableFuture saveByTenantId(AuditLog auditLog) { - return insertService.submit(() -> { + return service.submit(() -> { save(auditLog.getTenantId(), auditLog); return null; }); @@ -83,22 +76,22 @@ public class JpaAuditLogDao extends JpaAbstractDao imp @Override public ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog) { - return insertService.submit(() -> null); + return service.submit(() -> null); } @Override public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) { - return insertService.submit(() -> null); + return service.submit(() -> null); } @Override public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) { - return insertService.submit(() -> null); + return service.submit(() -> null); } @Override public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { - return insertService.submit(() -> null); + return service.submit(() -> null); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java new file mode 100644 index 0000000000..7bb98a9163 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java @@ -0,0 +1,91 @@ +/** + * 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.component; + +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.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.ComponentDescriptorEntity; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Slf4j +public abstract class AbstractComponentDescriptorInsertRepository implements ComponentDescriptorInsertRepository { + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + protected PlatformTransactionManager transactionManager; + + protected ComponentDescriptorEntity saveAndGet(ComponentDescriptorEntity entity, String insertOrUpdateOnPrimaryKeyConflict, String insertOrUpdateOnUniqueKeyConflict) { + ComponentDescriptorEntity componentDescriptorEntity = null; + TransactionStatus insertTransaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRED); + try { + componentDescriptorEntity = 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 Component Descriptor with id {}, name {} and entityType {}", throwable.getMessage(), entity.getId(), entity.getName(), entity.getType()); + TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + try { + componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); + } catch (Throwable th) { + log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getId(), entity.getName(), entity.getType()); + transactionManager.rollback(transaction); + } + transactionManager.commit(transaction); + } else { + log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getId(), entity.getName(), entity.getType()); + } + } + return componentDescriptorEntity; + } + + @Modifying + protected abstract ComponentDescriptorEntity doProcessSaveOrUpdate(ComponentDescriptorEntity entity, String query); + + protected Query getQuery(ComponentDescriptorEntity entity, String query) { + return entityManager.createNativeQuery(query, ComponentDescriptorEntity.class) + .setParameter("id", UUIDConverter.fromTimeUUID(entity.getId())) + .setParameter("actions", entity.getActions()) + .setParameter("clazz", entity.getClazz()) + .setParameter("configuration_descriptor", entity.getConfigurationDescriptor().toString()) + .setParameter("name", entity.getName()) + .setParameter("scope", entity.getScope().name()) + .setParameter("search_text", entity.getSearchText()) + .setParameter("type", entity.getType().name()); + } + + private ComponentDescriptorEntity processSaveOrUpdate(ComponentDescriptorEntity entity, String query) { + return doProcessSaveOrUpdate(entity, query); + } + + private TransactionStatus getTransactionStatus(int propagationRequired) { + DefaultTransactionDefinition insertDefinition = new DefaultTransactionDefinition(); + insertDefinition.setPropagationBehavior(propagationRequired); + return transactionManager.getTransaction(insertDefinition); + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaEnricher.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorInsertRepository.java similarity index 71% rename from common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaEnricher.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorInsertRepository.java index b9226bbd38..dd24e38c11 100644 --- a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaEnricher.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorInsertRepository.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.kafka; +package org.thingsboard.server.dao.sql.component; -import java.util.UUID; +import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; -public interface TbKafkaEnricher { +public interface ComponentDescriptorInsertRepository { - T enrich(T value, String responseTopic, UUID requestId); + ComponentDescriptorEntity saveOrUpdate(ComponentDescriptorEntity entity); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java new file mode 100644 index 0000000000..4ceb6cecc0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.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.component; + +import org.springframework.stereotype.Repository; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; +import org.thingsboard.server.dao.util.HsqlDao; +import org.thingsboard.server.dao.util.SqlDao; + +@SqlDao +@HsqlDao +@Repository +public class HsqlComponentDescriptorInsertRepository extends AbstractComponentDescriptorInsertRepository { + + private static final String P_KEY_CONFLICT_STATEMENT = "(component_descriptor.id=I.id)"; + private static final String UNQ_KEY_CONFLICT_STATEMENT = "(component_descriptor.clazz=I.clazz)"; + + 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 ComponentDescriptorEntity saveOrUpdate(ComponentDescriptorEntity entity) { + return saveAndGet(entity, INSERT_OR_UPDATE_ON_P_KEY_CONFLICT, INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT); + } + + @Override + protected ComponentDescriptorEntity doProcessSaveOrUpdate(ComponentDescriptorEntity entity, String query) { + getQuery(entity, query).executeUpdate(); + return entityManager.find(ComponentDescriptorEntity.class, UUIDConverter.fromTimeUUID(entity.getId())); + } + + private static String getInsertString(String conflictStatement) { + return "MERGE INTO component_descriptor USING (VALUES :id, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) I (id, actions, clazz, configuration_descriptor, name, scope, search_text, type) ON " + conflictStatement + " WHEN MATCHED THEN UPDATE SET component_descriptor.id = I.id, component_descriptor.actions = I.actions, component_descriptor.clazz = I.clazz, component_descriptor.configuration_descriptor = I.configuration_descriptor, component_descriptor.name = I.name, component_descriptor.scope = I.scope, component_descriptor.search_text = I.search_text, component_descriptor.type = I.type" + + " WHEN NOT MATCHED THEN INSERT (id, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (I.id, I.actions, I.clazz, I.configuration_descriptor, I.name, I.scope, I.search_text, I.type)"; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java index c66ea9c5cc..0b764eafd4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java @@ -51,6 +51,9 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao getEntityClass() { return ComponentDescriptorEntity.class; @@ -67,7 +70,9 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao { @Param("idOffset") String idOffset, Pageable pageable); + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + + "AND d.id > :idOffset ORDER BY d.id") + List findByTenantId(@Param("tenantId") String tenantId, + @Param("idOffset") String idOffset, + Pageable pageable); + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + "AND d.id > :idOffset ORDER BY d.id") diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index be0b4b89df..0355ca95f2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; @@ -65,12 +66,20 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao @Override public List findDevicesByTenantId(UUID tenantId, TextPageLink pageLink) { - return DaoUtil.convertDataList( - deviceRepository.findByTenantId( - fromTimeUUID(tenantId), - Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), - new PageRequest(0, pageLink.getLimit()))); + if (StringUtils.isEmpty(pageLink.getTextSearch())) { + return DaoUtil.convertDataList( + deviceRepository.findByTenantId( + fromTimeUUID(tenantId), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } else { + return DaoUtil.convertDataList( + deviceRepository.findByTenantId( + fromTimeUUID(tenantId), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java new file mode 100644 index 0000000000..60201fcf82 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java @@ -0,0 +1,90 @@ +/** + * 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.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 javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Slf4j +public abstract class AbstractEventInsertRepository implements EventInsertRepository { + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + protected PlatformTransactionManager transactionManager; + + 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/EventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java index 051453cf63..491c1fb8b6 100644 --- 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 @@ -15,82 +15,10 @@ */ 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; +public interface EventInsertRepository { -@Slf4j -@SqlDao -@Repository -public abstract class EventInsertRepository { + EventEntity saveOrUpdate(EventEntity entity); - @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 index e34cc7b70c..54b3da2efc 100644 --- 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 @@ -24,7 +24,7 @@ import org.thingsboard.server.dao.util.SqlDao; @SqlDao @HsqlDao @Repository -public class HsqlEventInsertRepository extends EventInsertRepository { +public class HsqlEventInsertRepository extends AbstractEventInsertRepository { 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)"; 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 index e4fd1ed37c..7e07e48983 100644 --- 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 @@ -25,7 +25,7 @@ import org.thingsboard.server.dao.util.SqlDao; @SqlDao @PsqlDao @Repository -public class PsqlEventInsertRepository extends EventInsertRepository { +public class PsqlEventInsertRepository extends AbstractEventInsertRepository { 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)"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractInsertRepository.java new file mode 100644 index 0000000000..e8134ca1a1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractInsertRepository.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.sqlts; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.regex.Pattern; + +@Repository +public abstract class AbstractInsertRepository { + + private static final ThreadLocal PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE))); + private static final String EMPTY_STR = ""; + + 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"; + + protected static final String TS_KV_LATEST_TABLE = "ts_kv_latest"; + protected static final String TS_KV_TABLE = "ts_kv"; + + protected static final String HSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, BOOL_V); + protected static final String HSQL_ON_STR_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, STR_V); + protected static final String HSQL_ON_LONG_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, LONG_V); + protected static final String HSQL_ON_DBL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, DBL_V); + + protected static final String HSQL_LATEST_ON_BOOL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, BOOL_V); + protected static final String HSQL_LATEST_ON_STR_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, STR_V); + protected static final String HSQL_LATEST_ON_LONG_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, LONG_V); + protected static final String HSQL_LATEST_ON_DBL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, DBL_V); + + protected static final String PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, dbl_v = null"; + protected static final String PSQL_ON_STR_VALUE_UPDATE_SET_NULLS = "bool_v = null, long_v = null, dbl_v = null"; + protected static final String PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS = "str_v = null, bool_v = null, dbl_v = null"; + protected static final String PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, bool_v = null"; + + @Value("${sql.remove_null_chars}") + private boolean removeNullChars; + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + protected JdbcTemplate jdbcTemplate; + + @Autowired + protected TransactionTemplate transactionTemplate; + + protected static String getInsertOrUpdateStringHsql(String tableName, String constraint, String value, String nullValues) { + return "MERGE INTO " + tableName + " USING(VALUES :entity_type, :entity_id, :key, :ts, :" + value + ") A (entity_type, entity_id, key, ts, " + value + ") ON " + constraint + " WHEN MATCHED THEN UPDATE SET " + tableName + "." + value + " = A." + value + ", " + tableName + ".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 + ")"; + } + + protected static String getInsertOrUpdateStringPsql(String tableName, String constraint, String value, String nullValues) { + return "INSERT INTO " + tableName + " (entity_type, entity_id, key, ts, " + value + ") VALUES (:entity_type, :entity_id, :key, :ts, :" + value + ") ON CONFLICT " + constraint + " DO UPDATE SET " + value + " = :" + value + ", ts = :ts," + nullValues; + } + + private static String getHsqlNullValues(String tableName, String notNullValue) { + switch (notNullValue) { + case BOOL_V: + return " " + tableName + ".str_v = null, " + tableName + ".long_v = null, " + tableName + ".dbl_v = null "; + case STR_V: + return " " + tableName + ".bool_v = null, " + tableName + ".long_v = null, " + tableName + ".dbl_v = null "; + case LONG_V: + return " " + tableName + ".str_v = null, " + tableName + ".bool_v = null, " + tableName + ".dbl_v = null "; + case DBL_V: + return " " + tableName + ".str_v = null, " + tableName + ".long_v = null, " + tableName + ".bool_v = null "; + default: + throw new RuntimeException("Unsupported insert value: [" + notNullValue + "]"); + } + } + + protected String replaceNullChars(String strValue) { + if (removeNullChars && strValue != null) { + return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR); + } + return strValue; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractLatestInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractLatestInsertRepository.java new file mode 100644 index 0000000000..e9b10eafa3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractLatestInsertRepository.java @@ -0,0 +1,58 @@ +/** + * 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.sqlts.ts.TsKvLatestEntity; + +import java.util.List; + +@Repository +public abstract class AbstractLatestInsertRepository extends AbstractInsertRepository { + + public abstract void saveOrUpdate(TsKvLatestEntity entity); + + public abstract void saveOrUpdate(List entities); + + protected void processSaveOrUpdate(TsKvLatestEntity 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(TsKvLatestEntity entity, String query); + + @Modifying + protected abstract void saveOrUpdateString(TsKvLatestEntity entity, String query); + + @Modifying + protected abstract void saveOrUpdateLong(TsKvLatestEntity entity, String query); + + @Modifying + protected abstract void saveOrUpdateDouble(TsKvLatestEntity entity, String query); + +} \ 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 index cf3596a864..a7efad1e88 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -70,7 +70,7 @@ public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningEx if (poolSize <= 0) { poolSize = maximumPoolSize * 4; } - insertService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(poolSize)); + insertService = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool(poolSize)); break; } } @@ -127,4 +127,4 @@ public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningEx 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 index 608933c3f7..4787cd7a46 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java @@ -17,24 +17,17 @@ 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 org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; +import java.util.List; @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 class AbstractTimeseriesInsertRepository extends AbstractInsertRepository { public abstract void saveOrUpdate(T entity); + public abstract void saveOrUpdate(List entities); + protected void processSaveOrUpdate(T entity, String requestBoolValue, String requestStrValue, String requestLongValue, String requestDblValue) { if (entity.getBooleanValue() != null) { saveOrUpdateBoolean(entity, requestBoolValue); 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 index 3c87e9f909..b47c79cb98 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sqlts.timescale; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvEntity; @@ -22,27 +23,81 @@ import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; import org.thingsboard.server.dao.util.PsqlDao; import org.thingsboard.server.dao.util.TimescaleDBTsDao; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + @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, PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, PSQL_ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V, PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS); + + private static final String BATCH_UPDATE = + "UPDATE tenant_ts_kv SET bool_v = ?, str_v = ?, long_v = ?, dbl_v = ? WHERE entity_type = ? AND entity_id = ? and key = ? and ts = ?"; + - 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 final String INSERT_OR_UPDATE = + "INSERT INTO tenant_ts_kv (tenant_id, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) VALUES(?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT (tenant_id, entity_id, key, ts) DO UPDATE SET bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?;"; @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 + public void saveOrUpdate(List entities) { + jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, entities.get(i).getTenantId()); + ps.setString(2, entities.get(i).getEntityId()); + ps.setString(3, entities.get(i).getKey()); + ps.setLong(4, entities.get(i).getTs()); + + if (entities.get(i).getBooleanValue() != null) { + ps.setBoolean(5, entities.get(i).getBooleanValue()); + ps.setBoolean(9, entities.get(i).getBooleanValue()); + } else { + ps.setNull(5, Types.BOOLEAN); + ps.setNull(9, Types.BOOLEAN); + } + + ps.setString(6, replaceNullChars(entities.get(i).getStrValue())); + ps.setString(10, replaceNullChars(entities.get(i).getStrValue())); + + + if (entities.get(i).getLongValue() != null) { + ps.setLong(7, entities.get(i).getLongValue()); + ps.setLong(11, entities.get(i).getLongValue()); + } else { + ps.setNull(7, Types.BIGINT); + ps.setNull(11, Types.BIGINT); + } + + if (entities.get(i).getDoubleValue() != null) { + ps.setDouble(8, entities.get(i).getDoubleValue()); + ps.setDouble(12, entities.get(i).getDoubleValue()); + } else { + ps.setNull(8, Types.DOUBLE); + ps.setNull(12, Types.DOUBLE); + } + } + + @Override + public int getBatchSize() { + return entities.size(); + } + }); + } + @Override protected void saveOrUpdateBoolean(TimescaleTsKvEntity entity, String query) { entityManager.createNativeQuery(query) @@ -61,7 +116,7 @@ public class TimescaleInsertRepository extends AbstractTimeseriesInsertRepositor .setParameter("entity_id", entity.getEntityId()) .setParameter("key", entity.getKey()) .setParameter("ts", entity.getTs()) - .setParameter("str_v", entity.getStrValue()) + .setParameter("str_v", replaceNullChars(entity.getStrValue())) .executeUpdate(); } 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 index 844f22a31c..961f545567 100644 --- 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 @@ -21,6 +21,7 @@ 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.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @@ -36,11 +37,16 @@ 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.sql.ScheduledLogExecutorComponent; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueue; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; 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 javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -66,6 +72,39 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements @Autowired private AbstractTimeseriesInsertRepository insertRepository; + @Autowired + ScheduledLogExecutorComponent logExecutor; + + @Value("${sql.ts_timescale.batch_size:1000}") + private int batchSize; + + @Value("${sql.ts_timescale.batch_max_delay:100}") + private long maxDelay; + + @Value("${sql.ts_timescale.stats_print_interval_ms:1000}") + private long statsPrintIntervalMs; + + private TbSqlBlockingQueue queue; + + @PostConstruct + private void init() { + TbSqlBlockingQueueParams params = TbSqlBlockingQueueParams.builder() + .logName("TS Timescale") + .batchSize(batchSize) + .maxDelay(maxDelay) + .statsPrintIntervalMs(statsPrintIntervalMs) + .build(); + queue = new TbSqlBlockingQueue<>(params); + queue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + } + + @PreDestroy + private void destroy() { + if (queue != null) { + queue.destroy(); + } + } + @Override public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { return processFindAllAsync(tenantId, entityId, queries); @@ -126,11 +165,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements 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; - }); + return queue.add(entity); } @Override @@ -209,7 +244,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) { List> result = new ArrayList<>(); timescaleTsKvEntities.forEach(entity -> { - if(entity != null && entity.isNotEmpty()) { + if (entity != null && entity.isNotEmpty()) { entity.setEntityId(entityIdStr); entity.setTenantId(tenantIdStr); entity.setKey(key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlLatestInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlLatestInsertRepository.java new file mode 100644 index 0000000000..12c9f9c0a4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlLatestInsertRepository.java @@ -0,0 +1,140 @@ +/** + * 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.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestEntity; +import org.thingsboard.server.dao.sqlts.AbstractLatestInsertRepository; +import org.thingsboard.server.dao.util.HsqlDao; +import org.thingsboard.server.dao.util.SqlTsDao; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +@SqlTsDao +@HsqlDao +@Repository +@Transactional +public class HsqlLatestInsertRepository extends AbstractLatestInsertRepository { + + private static final String TS_KV_LATEST_CONSTRAINT = "(ts_kv_latest.entity_type=A.entity_type AND ts_kv_latest.entity_id=A.entity_id AND ts_kv_latest.key=A.key)"; + + private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, BOOL_V, HSQL_LATEST_ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, STR_V, HSQL_LATEST_ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, LONG_V, HSQL_LATEST_ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, DBL_V, HSQL_LATEST_ON_DBL_VALUE_UPDATE_SET_NULLS); + + private static final String INSERT_OR_UPDATE = + "MERGE INTO ts_kv_latest USING(VALUES ?, ?, ?, ?, ?, ?, ?, ?) " + + "T (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " + + "ON (ts_kv_latest.entity_type=T.entity_type " + + "AND ts_kv_latest.entity_id=T.entity_id " + + "AND ts_kv_latest.key=T.key) " + + "WHEN MATCHED THEN UPDATE SET ts_kv_latest.ts = T.ts, ts_kv_latest.bool_v = T.bool_v, ts_kv_latest.str_v = T.str_v, ts_kv_latest.long_v = T.long_v, ts_kv_latest.dbl_v = T.dbl_v " + + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " + + "VALUES (T.entity_type, T.entity_id, T.key, T.ts, T.bool_v, T.str_v, T.long_v, T.dbl_v);"; + + @Override + public void saveOrUpdate(TsKvLatestEntity entity) { + processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT); + } + + @Override + public void saveOrUpdate(List entities) { + jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, entities.get(i).getEntityType().name()); + ps.setString(2, entities.get(i).getEntityId()); + ps.setString(3, entities.get(i).getKey()); + ps.setLong(4, entities.get(i).getTs()); + + if (entities.get(i).getBooleanValue() != null) { + ps.setBoolean(5, entities.get(i).getBooleanValue()); + } else { + ps.setNull(5, Types.BOOLEAN); + } + + ps.setString(6, entities.get(i).getStrValue()); + + if (entities.get(i).getLongValue() != null) { + ps.setLong(7, entities.get(i).getLongValue()); + } else { + ps.setNull(7, Types.BIGINT); + } + + if (entities.get(i).getDoubleValue() != null) { + ps.setDouble(8, entities.get(i).getDoubleValue()); + } else { + ps.setNull(8, Types.DOUBLE); + } + } + + @Override + public int getBatchSize() { + return entities.size(); + } + }); + } + + @Override + protected void saveOrUpdateBoolean(TsKvLatestEntity 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(TsKvLatestEntity 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(TsKvLatestEntity 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(TsKvLatestEntity 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/sqlts/ts/HsqlTimeseriesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java index 3ac5cc67a9..5d27b0d06f 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sqlts.ts; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; @@ -22,31 +23,78 @@ import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; import org.thingsboard.server.dao.util.HsqlDao; import org.thingsboard.server.dao.util.SqlTsDao; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + @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 TS_KV_CONSTRAINT = "(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)"; - 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 final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, BOOL_V, HSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, STR_V, HSQL_ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, LONG_V, HSQL_ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, DBL_V, HSQL_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 + ")"; - } + private static final String INSERT_OR_UPDATE = + "MERGE INTO ts_kv USING(VALUES ?, ?, ?, ?, ?, ?, ?, ?) " + + "T (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " + + "ON (ts_kv.entity_type=T.entity_type " + + "AND ts_kv.entity_id=T.entity_id " + + "AND ts_kv.key=T.key " + + "AND ts_kv.ts=T.ts) " + + "WHEN MATCHED THEN UPDATE SET ts_kv.bool_v = T.bool_v, ts_kv.str_v = T.str_v, ts_kv.long_v = T.long_v, ts_kv.dbl_v = T.dbl_v " + + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " + + "VALUES (T.entity_type, T.entity_id, T.key, T.ts, T.bool_v, T.str_v, T.long_v, T.dbl_v);"; @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 + public void saveOrUpdate(List entities) { + jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, entities.get(i).getEntityType().name()); + ps.setString(2, entities.get(i).getEntityId()); + ps.setString(3, entities.get(i).getKey()); + ps.setLong(4, entities.get(i).getTs()); + + if (entities.get(i).getBooleanValue() != null) { + ps.setBoolean(5, entities.get(i).getBooleanValue()); + } else { + ps.setNull(5, Types.BOOLEAN); + } + + ps.setString(6, entities.get(i).getStrValue()); + + if (entities.get(i).getLongValue() != null) { + ps.setLong(7, entities.get(i).getLongValue()); + } else { + ps.setNull(7, Types.BIGINT); + } + + if (entities.get(i).getDoubleValue() != null) { + ps.setDouble(8, entities.get(i).getDoubleValue()); + } else { + ps.setNull(8, Types.DOUBLE); + } + } + + @Override + public int getBatchSize() { + return entities.size(); + } + }); + } + @Override protected void saveOrUpdateBoolean(TsKvEntity entity, String query) { entityManager.createNativeQuery(query) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java index 9e4e281ebd..7c198d9a73 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java @@ -22,6 +22,7 @@ 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.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @@ -38,6 +39,10 @@ import org.thingsboard.server.dao.DaoUtil; 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.sql.ScheduledLogExecutorComponent; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueue; +import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; +import org.thingsboard.server.dao.sqlts.AbstractLatestInsertRepository; import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao; import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; import org.thingsboard.server.dao.timeseries.SimpleListenableFuture; @@ -45,6 +50,8 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; 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; @@ -69,6 +76,66 @@ public class JpaTimeseriesDao extends AbstractSqlTimeseriesDao implements Timese @Autowired private AbstractTimeseriesInsertRepository insertRepository; + @Autowired + private AbstractLatestInsertRepository insertLatestRepository; + + @Autowired + ScheduledLogExecutorComponent logExecutor; + + @Value("${sql.ts.batch_size:1000}") + private int tsBatchSize; + + @Value("${sql.ts.batch_max_delay:100}") + private long tsMaxDelay; + + @Value("${sql.ts.stats_print_interval_ms:1000}") + private long tsStatsPrintIntervalMs; + + @Value("${sql.ts_latest.batch_size:1000}") + private int tsLatestBatchSize; + + @Value("${sql.ts_latest.batch_max_delay:100}") + private long tsLatestMaxDelay; + + @Value("${sql.ts_latest.stats_print_interval_ms:1000}") + private long tsLatestStatsPrintIntervalMs; + + private TbSqlBlockingQueue tsQueue; + private TbSqlBlockingQueue tsLatestQueue; + + + @PostConstruct + private void init() { + TbSqlBlockingQueueParams tsParams = TbSqlBlockingQueueParams.builder() + .logName("TS") + .batchSize(tsBatchSize) + .maxDelay(tsMaxDelay) + .statsPrintIntervalMs(tsStatsPrintIntervalMs) + .build(); + tsQueue = new TbSqlBlockingQueue<>(tsParams); + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + + TbSqlBlockingQueueParams tsLatestParams = TbSqlBlockingQueueParams.builder() + .logName("TS Latest") + .batchSize(tsLatestBatchSize) + .maxDelay(tsLatestMaxDelay) + .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) + .build(); + tsLatestQueue = new TbSqlBlockingQueue<>(tsLatestParams); + tsLatestQueue.init(logExecutor, v -> insertLatestRepository.saveOrUpdate(v)); + } + + @PreDestroy + private void destroy() { + if (tsQueue != null) { + tsQueue.destroy(); + } + + if (tsLatestQueue != null) { + tsLatestQueue.destroy(); + } + } + @Override public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { return processFindAllAsync(tenantId, entityId, queries); @@ -262,10 +329,7 @@ public class JpaTimeseriesDao extends AbstractSqlTimeseriesDao implements Timese entity.setLongValue(tsKvEntry.getLongValue().orElse(null)); entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); log.trace("Saving entity: {}", entity); - return insertService.submit(() -> { - insertRepository.saveOrUpdate(entity); - return null; - }); + return tsQueue.add(entity); } @Override @@ -284,10 +348,7 @@ public class JpaTimeseriesDao extends AbstractSqlTimeseriesDao implements Timese latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null)); latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); - return insertService.submit(() -> { - tsKvLatestRepository.save(latestEntity); - return null; - }); + return tsLatestQueue.add(latestEntity); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlLatestInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlLatestInsertRepository.java new file mode 100644 index 0000000000..92252e9d18 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlLatestInsertRepository.java @@ -0,0 +1,205 @@ +/** + * 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.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestEntity; +import org.thingsboard.server.dao.sqlts.AbstractLatestInsertRepository; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.dao.util.SqlTsDao; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +@SqlTsDao +@PsqlDao +@Repository +@Transactional +public class PsqlLatestInsertRepository extends AbstractLatestInsertRepository { + + private static final String TS_KV_LATEST_CONSTRAINT = "(entity_type, entity_id, key)"; + + private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, BOOL_V, PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, STR_V, PSQL_ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, LONG_V, PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, DBL_V, PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS); + + private static final String BATCH_UPDATE = + "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ? WHERE entity_type = ? AND entity_id = ? and key = ?"; + + + private static final String INSERT_OR_UPDATE = + "INSERT INTO ts_kv_latest (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) VALUES(?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT (entity_type, entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?;"; + + @Override + public void saveOrUpdate(TsKvLatestEntity entity) { + processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT); + } + + @Override + public void saveOrUpdate(List entities) { + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setLong(1, entities.get(i).getTs()); + + if (entities.get(i).getBooleanValue() != null) { + ps.setBoolean(2, entities.get(i).getBooleanValue()); + } else { + ps.setNull(2, Types.BOOLEAN); + } + + ps.setString(3, replaceNullChars(entities.get(i).getStrValue())); + + if (entities.get(i).getLongValue() != null) { + ps.setLong(4, entities.get(i).getLongValue()); + } else { + ps.setNull(4, Types.BIGINT); + } + + if (entities.get(i).getDoubleValue() != null) { + ps.setDouble(5, entities.get(i).getDoubleValue()); + } else { + ps.setNull(5, Types.DOUBLE); + } + + ps.setString(6, entities.get(i).getEntityType().name()); + ps.setString(7, entities.get(i).getEntityId()); + ps.setString(8, entities.get(i).getKey()); + } + + @Override + public int getBatchSize() { + return entities.size(); + } + }); + + int updatedCount = 0; + for (int i = 0; i < result.length; i++) { + if (result[i] == 0) { + updatedCount++; + } + } + + List insertEntities = new ArrayList<>(updatedCount); + for (int i = 0; i < result.length; i++) { + if (result[i] == 0) { + insertEntities.add(entities.get(i)); + } + } + + jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, insertEntities.get(i).getEntityType().name()); + ps.setString(2, insertEntities.get(i).getEntityId()); + ps.setString(3, insertEntities.get(i).getKey()); + ps.setLong(4, insertEntities.get(i).getTs()); + ps.setLong(9, insertEntities.get(i).getTs()); + + if (insertEntities.get(i).getBooleanValue() != null) { + ps.setBoolean(5, insertEntities.get(i).getBooleanValue()); + ps.setBoolean(10, insertEntities.get(i).getBooleanValue()); + } else { + ps.setNull(5, Types.BOOLEAN); + ps.setNull(10, Types.BOOLEAN); + } + + ps.setString(6, replaceNullChars(entities.get(i).getStrValue())); + ps.setString(11, replaceNullChars(entities.get(i).getStrValue())); + + + if (insertEntities.get(i).getLongValue() != null) { + ps.setLong(7, insertEntities.get(i).getLongValue()); + ps.setLong(12, insertEntities.get(i).getLongValue()); + } else { + ps.setNull(7, Types.BIGINT); + ps.setNull(12, Types.BIGINT); + } + + if (insertEntities.get(i).getDoubleValue() != null) { + ps.setDouble(8, insertEntities.get(i).getDoubleValue()); + ps.setDouble(13, insertEntities.get(i).getDoubleValue()); + } else { + ps.setNull(8, Types.DOUBLE); + ps.setNull(13, Types.DOUBLE); + } + } + + @Override + public int getBatchSize() { + return insertEntities.size(); + } + }); + } + }); + } + + @Override + protected void saveOrUpdateBoolean(TsKvLatestEntity 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(TsKvLatestEntity 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", replaceNullChars(entity.getStrValue())) + .executeUpdate(); + } + + @Override + protected void saveOrUpdateLong(TsKvLatestEntity 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(TsKvLatestEntity 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/sqlts/ts/PsqlTimeseriesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlTimeseriesInsertRepository.java index 4ed91c28f2..edc37822b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlTimeseriesInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlTimeseriesInsertRepository.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sqlts.ts; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; @@ -22,25 +23,27 @@ import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository; import org.thingsboard.server.dao.util.PsqlDao; import org.thingsboard.server.dao.util.SqlTsDao; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + @SqlTsDao @PsqlDao @Repository @Transactional public class PsqlTimeseriesInsertRepository 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 TS_KV_CONSTRAINT = "(entity_type, entity_id, key, ts)"; - 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 final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, BOOL_V, PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, STR_V, PSQL_ON_STR_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, LONG_V, PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS); + private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, DBL_V, PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS); - private static String getInsertOrUpdateString(String value, String nullValues) { - return "INSERT INTO ts_kv (entity_type, entity_id, key, ts, " + value + ") VALUES (:entity_type, :entity_id, :key, :ts, :" + value + ") ON CONFLICT (entity_type, entity_id, key, ts) DO UPDATE SET " + value + " = :" + value + ", ts = :ts," + nullValues; - } + private static final String INSERT_OR_UPDATE = + "INSERT INTO ts_kv (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) VALUES(?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT (entity_type, entity_id, key, ts) DO UPDATE SET bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?;"; @Override public void saveOrUpdate(TsKvEntity entity) { @@ -65,7 +68,7 @@ public class PsqlTimeseriesInsertRepository extends AbstractTimeseriesInsertRepo .setParameter("entity_id", entity.getEntityId()) .setParameter("key", entity.getKey()) .setParameter("ts", entity.getTs()) - .setParameter("str_v", entity.getStrValue()) + .setParameter("str_v", replaceNullChars(entity.getStrValue())) .executeUpdate(); } @@ -90,4 +93,50 @@ public class PsqlTimeseriesInsertRepository extends AbstractTimeseriesInsertRepo .setParameter("dbl_v", entity.getDoubleValue()) .executeUpdate(); } + + @Override + public void saveOrUpdate(List entities) { + jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setString(1, entities.get(i).getEntityType().name()); + ps.setString(2, entities.get(i).getEntityId()); + ps.setString(3, entities.get(i).getKey()); + ps.setLong(4, entities.get(i).getTs()); + + if (entities.get(i).getBooleanValue() != null) { + ps.setBoolean(5, entities.get(i).getBooleanValue()); + ps.setBoolean(9, entities.get(i).getBooleanValue()); + } else { + ps.setNull(5, Types.BOOLEAN); + ps.setNull(9, Types.BOOLEAN); + } + + ps.setString(6, replaceNullChars(entities.get(i).getStrValue())); + ps.setString(10, replaceNullChars(entities.get(i).getStrValue())); + + + if (entities.get(i).getLongValue() != null) { + ps.setLong(7, entities.get(i).getLongValue()); + ps.setLong(11, entities.get(i).getLongValue()); + } else { + ps.setNull(7, Types.BIGINT); + ps.setNull(11, Types.BIGINT); + } + + if (entities.get(i).getDoubleValue() != null) { + ps.setDouble(8, entities.get(i).getDoubleValue()); + ps.setDouble(12, entities.get(i).getDoubleValue()); + } else { + ps.setNull(8, Types.DOUBLE); + ps.setNull(12, Types.DOUBLE); + } + } + + @Override + public int getBatchSize() { + return entities.size(); + } + }); + } } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index c22321ee52..a553aa5f9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -44,6 +44,7 @@ public abstract class AbstractBufferedRateExecutor perTenantLimits = new ConcurrentHashMap<>(); @@ -57,12 +58,14 @@ public abstract class AbstractBufferedRateExecutor(queueLimit); this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads); this.callbackExecutor = Executors.newWorkStealingPool(callbackThreads); @@ -131,6 +134,13 @@ public abstract class AbstractBufferedRateExecutor finalTaskCtx = taskCtx; + if (printQueriesFreq > 0) { + if (printQueriesIdx.incrementAndGet() >= printQueriesFreq) { + printQueriesIdx.set(0); + String query = queryToString(finalTaskCtx); + log.info("[{}] Cassandra query: {}", taskCtx.getId(), query); + } + } logTask("Processing", finalTaskCtx); concurrencyLevel.incrementAndGet(); long timeout = finalTaskCtx.getCreateTime() + maxWaitTime - System.currentTimeMillis(); @@ -187,17 +197,8 @@ public abstract class AbstractBufferedRateExecutor taskCtx) { if (log.isTraceEnabled()) { if (taskCtx.getTask() instanceof CassandraStatementTask) { - CassandraStatementTask cassStmtTask = (CassandraStatementTask) taskCtx.getTask(); - if (cassStmtTask.getStatement() instanceof BoundStatement) { - BoundStatement stmt = (BoundStatement) cassStmtTask.getStatement(); - String query = stmt.preparedStatement().getQueryString(); - try { - query = toStringWithValues(stmt, ProtocolVersion.V5); - } catch (Exception e) { - log.warn("Can't convert to query with values", e); - } - log.trace("[{}] {} task: {}, BoundStatement query: {}", taskCtx.getId(), action, taskCtx, query); - } + String query = queryToString(taskCtx); + log.trace("[{}] {} task: {}, BoundStatement query: {}", taskCtx.getId(), action, taskCtx, query); } else { log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx); } @@ -206,6 +207,22 @@ public abstract class AbstractBufferedRateExecutor taskCtx) { + CassandraStatementTask cassStmtTask = (CassandraStatementTask) taskCtx.getTask(); + if (cassStmtTask.getStatement() instanceof BoundStatement) { + BoundStatement stmt = (BoundStatement) cassStmtTask.getStatement(); + String query = stmt.preparedStatement().getQueryString(); + try { + query = toStringWithValues(stmt, ProtocolVersion.V5); + } catch (Exception e) { + log.warn("Can't convert to query with values", e); + } + return query; + } else { + return "Not Cassandra Statement Task"; + } + } + private static String toStringWithValues(BoundStatement boundStatement, ProtocolVersion protocolVersion) { CodecRegistry codecRegistry = boundStatement.preparedStatement().getCodecRegistry(); PreparedStatement preparedStatement = boundStatement.preparedStatement(); diff --git a/dao/src/main/resources/cassandra/schema-entities.cql b/dao/src/main/resources/cassandra/schema-entities.cql index 611c08d5ef..a07e27cbe1 100644 --- a/dao/src/main/resources/cassandra/schema-entities.cql +++ b/dao/src/main/resources/cassandra/schema-entities.cql @@ -244,6 +244,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.asset ( customer_id timeuuid, name text, type text, + label text, search_text text, additional_info text, PRIMARY KEY (id, tenant_id, customer_id, type) @@ -711,4 +712,4 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_ent AND search_text IS NOT NULL AND id IS NOT NULL PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id, type) - WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC); \ No newline at end of file + WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC); diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 59785ae758..a0a25ee070 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -14,10 +14,22 @@ -- limitations under the License. -- -CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(tenant_id, type, originator_type, originator_id); +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(originator_id, type, start_ts DESC); CREATE INDEX IF NOT EXISTS idx_event_type_entity_id ON event(tenant_id, event_type, entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); CREATE INDEX IF NOT EXISTS idx_relation_from_id ON relation(relation_type_group, from_type, from_id); + +CREATE INDEX IF NOT EXISTS idx_device_customer_id ON device(tenant_id, customer_id); + +CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id, customer_id, type); + +CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type); + +CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id); + +CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type); + +CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); \ No newline at end of file diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 2903ca2fb1..4b21d851a5 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -42,9 +42,11 @@ CREATE TABLE IF NOT EXISTS asset ( additional_info varchar, customer_id varchar(31), name varchar(255), + label varchar(255), search_text varchar(255), tenant_id varchar(31), - type varchar(255) + type varchar(255), + CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name) ); CREATE TABLE IF NOT EXISTS audit_log ( @@ -119,7 +121,8 @@ CREATE TABLE IF NOT EXISTS device ( name varchar(255), label varchar(255), search_text varchar(255), - tenant_id varchar(31) + tenant_id varchar(31), + CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name) ); CREATE TABLE IF NOT EXISTS device_credentials ( @@ -127,7 +130,8 @@ CREATE TABLE IF NOT EXISTS device_credentials ( credentials_id varchar, credentials_type varchar(255), credentials_value varchar, - device_id varchar(31) + device_id varchar(31), + CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id) ); CREATE TABLE IF NOT EXISTS event ( diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 263da99a4b..caec9e0e66 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -40,3 +40,5 @@ security.claim.allowClaimingByDefault=true security.claim.duration=60000 database.ts_max_intervals=700 + +sql.remove_null_chars=true diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index a6b5d65ff7..c706d0dc37 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 msa org.thingsboard.msa diff --git a/msa/js-executor/api/jsExecutor.js b/msa/js-executor/api/jsExecutor.js index 3568a6c410..7a4ffb04f1 100644 --- a/msa/js-executor/api/jsExecutor.js +++ b/msa/js-executor/api/jsExecutor.js @@ -17,11 +17,28 @@ const vm = require('vm'); -function JsExecutor() { +function JsExecutor(useSandbox) { + this.useSandbox = useSandbox; } JsExecutor.prototype.compileScript = function(code) { - return new Promise(function(resolve, reject) { + if (this.useSandbox) { + return createScript(code); + } else { + return createFunction(code); + } +} + +JsExecutor.prototype.executeScript = function(script, args, timeout) { + if (this.useSandbox) { + return invokeScript(script, args, timeout); + } else { + return invokeFunction(script, args); + } +} + +function createScript(code) { + return new Promise((resolve, reject) => { try { code = "("+code+")(...args)"; var script = new vm.Script(code); @@ -32,8 +49,8 @@ JsExecutor.prototype.compileScript = function(code) { }); } -JsExecutor.prototype.executeScript = function(script, args, timeout) { - return new Promise(function(resolve, reject) { +function invokeScript(script, args, timeout) { + return new Promise((resolve, reject) => { try { var sandbox = Object.create(null); sandbox.args = args; @@ -45,4 +62,29 @@ JsExecutor.prototype.executeScript = function(script, args, timeout) { }); } + +function createFunction(code) { + return new Promise((resolve, reject) => { + try { + code = "return ("+code+")(...args)"; + const parsingContext = vm.createContext({}); + const func = vm.compileFunction(code, ['args'], {parsingContext: parsingContext}); + resolve(func); + } catch (err) { + reject(err); + } + }); +} + +function invokeFunction(func, args) { + return new Promise((resolve, reject) => { + try { + var result = func(args); + resolve(result); + } catch (err) { + reject(err); + } + }); +} + module.exports = JsExecutor; diff --git a/msa/js-executor/api/jsInvokeMessageProcessor.js b/msa/js-executor/api/jsInvokeMessageProcessor.js index 1a1df75e09..6afc02e89c 100644 --- a/msa/js-executor/api/jsInvokeMessageProcessor.js +++ b/msa/js-executor/api/jsInvokeMessageProcessor.js @@ -15,18 +15,22 @@ */ 'use strict'; +const COMPILATION_ERROR = 0; +const RUNTIME_ERROR = 1; +const TIMEOUT_ERROR = 2; +const UNRECOGNIZED = -1; + const config = require('config'), - logger = require('../config/logger')('JsInvokeMessageProcessor'), + logger = require('../config/logger')._logger('JsInvokeMessageProcessor'), Utils = require('./utils'), - js = require('./jsinvoke.proto').js, - KeyedMessage = require('kafka-node').KeyedMessage, JsExecutor = require('./jsExecutor'); const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency')); +const useSandbox = config.get('script.use_sandbox') === 'true'; function JsInvokeMessageProcessor(producer) { this.producer = producer; - this.executor = new JsExecutor(); + this.executor = new JsExecutor(useSandbox); this.scriptMap = {}; this.executedScriptsCounter = 0; } @@ -34,18 +38,22 @@ function JsInvokeMessageProcessor(producer) { JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { var requestId; + var responseTopic; try { - var request = js.RemoteJsRequest.decode(message.value); - requestId = getRequestId(request); + var request = JSON.parse(message.value.toString('utf8')); + var buf = message.headers['requestId']; + requestId = Utils.UUIDFromBuffer(buf); + buf = message.headers['responseTopic']; + responseTopic = buf.toString('utf8'); - logger.debug('[%s] Received request, responseTopic: [%s]', requestId, request.responseTopic); + logger.debug('[%s] Received request, responseTopic: [%s]', requestId, responseTopic); if (request.compileRequest) { - this.processCompileRequest(requestId, request.responseTopic, request.compileRequest); + this.processCompileRequest(requestId, responseTopic, request.compileRequest); } else if (request.invokeRequest) { - this.processInvokeRequest(requestId, request.responseTopic, request.invokeRequest); + this.processInvokeRequest(requestId, responseTopic, request.invokeRequest); } else if (request.releaseRequest) { - this.processReleaseRequest(requestId, request.responseTopic, request.releaseRequest); + this.processReleaseRequest(requestId, responseTopic, request.releaseRequest); } else { logger.error('[%s] Unknown request recevied!', requestId); } @@ -68,7 +76,7 @@ JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, r this.sendResponse(requestId, responseTopic, scriptId, compileResponse); }, (err) => { - var compileResponse = createCompileResponse(scriptId, false, js.JsInvokeErrorCode.COMPILATION_ERROR, err); + var compileResponse = createCompileResponse(scriptId, false, COMPILATION_ERROR, err); logger.debug('[%s] Sending failed compile response, scriptId: [%s]', requestId, scriptId); this.sendResponse(requestId, responseTopic, scriptId, compileResponse); } @@ -96,9 +104,9 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re (err) => { var errorCode; if (err.message.includes('Script execution timed out')) { - errorCode = js.JsInvokeErrorCode.TIMEOUT_ERROR; + errorCode = TIMEOUT_ERROR; } else { - errorCode = js.JsInvokeErrorCode.RUNTIME_ERROR; + errorCode = RUNTIME_ERROR; } var invokeResponse = createInvokeResponse("", false, errorCode, err); logger.debug('[%s] Sending failed invoke response, scriptId: [%s], errorCode: [%s]', requestId, scriptId, errorCode); @@ -107,8 +115,8 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re ) }, (err) => { - var invokeResponse = createInvokeResponse("", false, js.JsInvokeErrorCode.COMPILATION_ERROR, err); - logger.debug('[%s] Sending failed invoke response, scriptId: [%s], errorCode: [%s]', requestId, scriptId, js.JsInvokeErrorCode.COMPILATION_ERROR); + var invokeResponse = createInvokeResponse("", false, COMPILATION_ERROR, err); + logger.debug('[%s] Sending failed invoke response, scriptId: [%s], errorCode: [%s]', requestId, scriptId, COMPILATION_ERROR); this.sendResponse(requestId, responseTopic, scriptId, null, invokeResponse); } ); @@ -127,15 +135,26 @@ JsInvokeMessageProcessor.prototype.processReleaseRequest = function(requestId, r JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseTopic, scriptId, compileResponse, invokeResponse, releaseResponse) { var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse); - var rawResponse = js.RemoteJsResponse.encode(remoteResponse).finish(); - const message = new KeyedMessage(scriptId, rawResponse); - const payloads = [ { topic: responseTopic, messages: message, key: scriptId } ]; - this.producer.send(payloads, function (err, data) { - if (err) { - logger.error('[%s] Failed to send response to kafka: %s', requestId, err.message); - logger.error(err.stack); + var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8'); + this.producer.send( + { + topic: responseTopic, + messages: [ + { + key: scriptId, + value: rawResponse + } + ] } - }); + ).then( + () => {}, + (err) => { + if (err) { + logger.error('[%s] Failed to send response to kafka: %s', requestId, err.message); + logger.error(err.stack); + } + } + ); } JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scriptBody) { @@ -159,50 +178,42 @@ JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scrip function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) { const requestIdBits = Utils.UUIDToBits(requestId); - return js.RemoteJsResponse.create( - { + return { requestIdMSB: requestIdBits[0], requestIdLSB: requestIdBits[1], compileResponse: compileResponse, invokeResponse: invokeResponse, releaseResponse: releaseResponse - } - ); + }; } function createCompileResponse(scriptId, success, errorCode, err) { const scriptIdBits = Utils.UUIDToBits(scriptId); - return js.JsCompileResponse.create( - { + return { errorCode: errorCode, success: success, errorDetails: parseJsErrorDetails(err), scriptIdMSB: scriptIdBits[0], scriptIdLSB: scriptIdBits[1] - } - ); + }; } function createInvokeResponse(result, success, errorCode, err) { - return js.JsInvokeResponse.create( - { + return { errorCode: errorCode, success: success, errorDetails: parseJsErrorDetails(err), result: result - } - ); + }; } function createReleaseResponse(scriptId, success) { const scriptIdBits = Utils.UUIDToBits(scriptId); - return js.JsReleaseResponse.create( - { + return { success: success, scriptIdMSB: scriptIdBits[0], scriptIdLSB: scriptIdBits[1] - } - ); + }; } function parseJsErrorDetails(err) { @@ -229,8 +240,4 @@ function getScriptId(request) { return Utils.toUUIDString(request.scriptIdMSB, request.scriptIdLSB); } -function getRequestId(request) { - return Utils.toUUIDString(request.requestIdMSB, request.requestIdLSB); -} - -module.exports = JsInvokeMessageProcessor; \ No newline at end of file +module.exports = JsInvokeMessageProcessor; diff --git a/msa/js-executor/api/utils.js b/msa/js-executor/api/utils.js index 40ab74e721..2e0381086b 100644 --- a/msa/js-executor/api/utils.js +++ b/msa/js-executor/api/utils.js @@ -18,16 +18,17 @@ const Long = require('long'), uuidParse = require('uuid-parse'); -var logger = require('../config/logger')('Utils'); - exports.toUUIDString = function(mostSigBits, leastSigBits) { var msbBytes = Long.fromValue(mostSigBits, false).toBytes(false); var lsbBytes = Long.fromValue(leastSigBits, false).toBytes(false); var uuidBytes = msbBytes.concat(lsbBytes); - var buff = new Buffer(uuidBytes, 'utf8'); return uuidParse.unparse(uuidBytes); } +exports.UUIDFromBuffer = function(buf) { + return uuidParse.unparse(buf); +} + exports.UUIDToBits = function(uuidString) { const bytes = uuidParse.parse(uuidString); var msb = Long.fromBytes(bytes.slice(0,8), false, false).toString(); diff --git a/msa/js-executor/config/custom-environment-variables.yml b/msa/js-executor/config/custom-environment-variables.yml index 99f47cb5f1..c3caf15a9a 100644 --- a/msa/js-executor/config/custom-environment-variables.yml +++ b/msa/js-executor/config/custom-environment-variables.yml @@ -25,4 +25,5 @@ logger: filename: "LOGGER_FILENAME" script: + use_sandbox: "SCRIPT_USE_SANDBOX" script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY" diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index fb470567e7..9688722109 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -26,4 +26,5 @@ logger: filename: "tb-js-executor-%DATE%.log" script: + use_sandbox: "true" script_body_trace_frequency: "1000" diff --git a/msa/js-executor/config/logger.js b/msa/js-executor/config/logger.js index 4f5340c2a8..317dc0cdd5 100644 --- a/msa/js-executor/config/logger.js +++ b/msa/js-executor/config/logger.js @@ -17,9 +17,24 @@ var config = require('config'), path = require('path'), DailyRotateFile = require('winston-daily-rotate-file'); +const { logLevel } = require('kafkajs'); const { createLogger, format, transports } = require('winston'); const { combine, timestamp, label, printf, splat } = format; +const toWinstonLogLevel = level => { + switch(level) { + case logLevel.ERROR: + case logLevel.NOTHING: + return 'error' + case logLevel.WARN: + return 'warn' + case logLevel.INFO: + return 'info' + case logLevel.DEBUG: + return 'debug' + } +} + var loggerTransports = []; if (process.env.NODE_ENV !== 'production' || process.env.DOCKER_MODE === 'true') { @@ -56,4 +71,33 @@ function _logger(moduleLabel) { }); } -module.exports = _logger; \ No newline at end of file +const KafkaJsWinstonLogCreator = logLevel => { + const logger = createLogger({ + level: toWinstonLogLevel(logLevel), + format:combine( + splat(), + label({ label: 'kafkajs' }), + timestamp({format: 'YYYY-MM-DD HH:mm:ss,SSS'}), + printf(info => { + var res = `${info.timestamp} [${info.label}] ${info.level.toUpperCase()}: ${info.message}`; + if (info.extra) { + res +=`: ${JSON.stringify(info.extra)}`; + } + return res; + } + ) + ), + transports: loggerTransports + }); + + return ({ namespace, level, label, log }) => { + const { message, ...extra } = log; + logger.log({ + level: toWinstonLogLevel(level), + message, + extra, + }); + } +} + +module.exports = {_logger, KafkaJsWinstonLogCreator}; diff --git a/msa/js-executor/package-lock.json b/msa/js-executor/package-lock.json index c61ef011a2..96c8341882 100644 --- a/msa/js-executor/package-lock.json +++ b/msa/js-executor/package-lock.json @@ -1,6 +1,6 @@ { "name": "thingsboard-js-executor", - "version": "2.4.1", + "version": "2.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -35,60 +35,6 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -106,11 +52,6 @@ "@types/node": "*" } }, - "@types/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" - }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -120,7 +61,8 @@ "@types/node": { "version": "10.12.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.10.tgz", - "integrity": "sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w==" + "integrity": "sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w==", + "dev": true }, "abbrev": { "version": "1.1.1", @@ -182,11 +124,6 @@ } } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -206,22 +143,6 @@ "normalize-path": "^2.1.1" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -331,7 +252,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base": { "version": "0.11.2", @@ -397,39 +319,12 @@ "tweetnacl": "^0.14.3" } }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", "dev": true }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", - "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -482,6 +377,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -516,53 +412,6 @@ } } }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "optional": true, - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "optional": true - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "optional": true - }, - "buffermaker": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/buffermaker/-/buffermaker-1.2.1.tgz", - "integrity": "sha512-IdnyU2jDHU65U63JuVQNTHiWjPRH0CS3aYd/WPaEwyX84rFdukhOduAVb1jwUScmb5X0JWPw8NZOrhoLMiyAHQ==", - "requires": { - "long": "1.1.2" - }, - "dependencies": { - "long": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/long/-/long-1.1.2.tgz", - "integrity": "sha1-6u9ZUcp1UdlpJrgtokLbnWso+1M=" - } - } - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" - }, "byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -610,14 +459,6 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -650,12 +491,6 @@ "upath": "^1.0.5" } }, - "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", - "optional": true - }, "ci-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", @@ -691,11 +526,6 @@ "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -774,7 +604,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "config": { "version": "3.2.2", @@ -798,11 +629,6 @@ "xdg-basedir": "^3.0.0" } }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -858,6 +684,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -868,19 +695,11 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "optional": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true }, "deep-is": { "version": "0.1.3", @@ -935,23 +754,6 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true - }, - "denque": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true - }, "diagnostics": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", @@ -1004,14 +806,6 @@ "env-variable": "0.0.x" } }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, "env-variable": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", @@ -1272,12 +1066,6 @@ "moment": "^2.11.2" } }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true - }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1343,12 +1131,6 @@ "readable-stream": "^2.0.0" } }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "optional": true - }, "fs-extra": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", @@ -1895,22 +1677,6 @@ } } }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, "get-stream": { "version": "3.0.0", "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -1932,12 +1698,6 @@ "assert-plus": "^1.0.0" } }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "optional": true - }, "glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", @@ -2059,12 +1819,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true - }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -2150,7 +1904,8 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true }, "into-stream": { "version": "5.1.0", @@ -2262,14 +2017,6 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, "is-glob": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", @@ -2453,42 +2200,12 @@ "verror": "1.10.0" } }, - "kafka-node": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/kafka-node/-/kafka-node-4.1.3.tgz", - "integrity": "sha512-C2WHksRCr7vIKmbxYaCk2c5Q1lnHIi6C0f3AioK3ARcRHGO9DpqErcoaS9d8PP62yzTnkYras+iAlmPsZHNSfw==", - "requires": { - "async": "^2.6.2", - "binary": "~0.3.0", - "bl": "^2.2.0", - "buffer-crc32": "~0.2.5", - "buffermaker": "~1.2.0", - "debug": "^2.1.3", - "denque": "^1.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", - "nested-error-stacks": "^2.0.0", - "optional": "^0.1.3", - "retry": "^0.10.1", - "snappy": "^6.0.1", - "uuid": "^3.0.0" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - } - } - } + "kafkajs": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-1.11.0.tgz", + "integrity": "sha512-dLRCcFIBygZucR+e8U2ZqH2wgMrAu114K0szUyUseJoeOii3cG5bHZPIdqKecXxI6begPVCfGS3R0nJY4zHW2A==", + "requires": { + "long": "^4.0.0" } }, "kind-of": { @@ -2641,16 +2358,11 @@ "mime-db": "1.40.0" } }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "optional": true - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2685,6 +2397,7 @@ "version": "0.5.1", "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, "requires": { "minimist": "0.0.8" }, @@ -2692,7 +2405,8 @@ "minimist": { "version": "0.0.8", "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true } } }, @@ -2704,7 +2418,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multistream": { "version": "2.1.1", @@ -2742,26 +2457,6 @@ "to-regex": "^3.0.1" } }, - "napi-build-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", - "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==", - "optional": true - }, - "nested-error-stacks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", - "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==" - }, - "node-abi": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.9.0.tgz", - "integrity": "sha512-jmEOvv0eanWjhX8dX1pmjb7oJl1U1oR4FOh0b2GnvALwSYoOdU7sj+kLDSAyjo4pfC9aj/IxkloxdLJQhSSQBA==", - "optional": true, - "requires": { - "semver": "^5.4.1" - } - }, "nodemon": { "version": "1.18.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.7.tgz", @@ -2797,12 +2492,6 @@ } } }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", - "optional": true - }, "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", @@ -2830,35 +2519,12 @@ "path-key": "^2.0.0" } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "optional": true - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -2917,6 +2583,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -2926,11 +2593,6 @@ "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" }, - "optional": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz", - "integrity": "sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==" - }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -2945,12 +2607,6 @@ "wordwrap": "~1.0.0" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "optional": true - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -3127,38 +2783,6 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "prebuild-install": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.0.tgz", - "integrity": "sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==", - "optional": true, - "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", - "os-homedir": "^1.0.1", - "pump": "^2.0.1", - "rc": "^1.2.7", - "simple-get": "^2.7.0", - "tar-fs": "^1.13.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "optional": true - } - } - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -3182,26 +2806,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - } - }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -3220,16 +2824,6 @@ "integrity": "sha512-vL6NLxNHzkNTjGJUpMm5PLC+94/0tTlC1vkP9bdU0pOHih+EujMjgMTwfZopZvHWRFbqJ5Y73OMoau50PewDDA==", "dev": true }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "optional": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -3246,6 +2840,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -3389,11 +2984,6 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, - "retry": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", - "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3428,12 +3018,6 @@ "semver": "^5.0.3" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "optional": true - }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", @@ -3475,24 +3059,8 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", - "optional": true - }, - "simple-get": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", - "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", - "optional": true, - "requires": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true }, "simple-swizzle": { "version": "0.2.2", @@ -3615,25 +3183,6 @@ } } }, - "snappy": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/snappy/-/snappy-6.2.3.tgz", - "integrity": "sha512-HZpVoIxMfQ4fL3iDuMdI1R5xycw1o9YDCAndTKZCY/EHRoKFvzwplttuBBVGeEg2fd1hYiwAXos/sM24W7N1LA==", - "optional": true, - "requires": { - "bindings": "^1.3.1", - "nan": "^2.14.0", - "prebuild-install": "^5.2.2" - }, - "dependencies": { - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "optional": true - } - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3725,16 +3274,6 @@ "readable-stream": "^2.1.4" } }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3743,14 +3282,6 @@ "safe-buffer": "~5.1.0" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, "strip-eof": { "version": "1.0.0", "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -3760,7 +3291,8 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true }, "supports-color": { "version": "5.5.0", @@ -3771,57 +3303,6 @@ "has-flag": "^3.0.0" } }, - "tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", - "optional": true, - "requires": { - "chownr": "^1.0.1", - "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" - }, - "dependencies": { - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "optional": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "optional": true, - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "dependencies": { - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "optional": true, - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - } - } - }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -3848,12 +3329,6 @@ "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "optional": true - }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -3923,11 +3398,6 @@ } } }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" - }, "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -3937,6 +3407,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -4140,7 +3611,8 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true }, "uuid-parse": { "version": "1.0.0", @@ -4167,21 +3639,6 @@ "isexe": "^2.0.0" } }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "widest-line": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", @@ -4281,7 +3738,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write-file-atomic": { "version": "2.3.0", @@ -4300,12 +3758,6 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "optional": true - }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index d61162dd5b..da4eb09b09 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,23 +1,21 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "2.4.1", + "version": "2.4.2", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.js", "bin": "server.js", "scripts": { - "build-proto": "pbjs -t static-module -w commonjs -o ./api/jsinvoke.proto.js ../../application/src/main/proto/jsinvoke.proto", - "install": "npm run build-proto && pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js", + "install": "pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js", "test": "echo \"Error: no test specified\" && exit 1", - "start": "npm run build-proto && nodemon server.js", - "start-prod": "npm run build-proto && NODE_ENV=production nodemon server.js" + "start": "nodemon server.js", + "start-prod": "NODE_ENV=production nodemon server.js" }, "dependencies": { "config": "^3.2.2", "js-yaml": "^3.12.0", - "kafka-node": "^4.1.3", + "kafkajs": "^1.11.0", "long": "^4.0.0", - "protobufjs": "^6.8.8", "uuid-parse": "^1.0.0", "winston": "^3.0.0", "winston-daily-rotate-file": "^3.2.1" diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 27636dbecd..53616e6493 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 msa org.thingsboard.msa diff --git a/msa/js-executor/server.js b/msa/js-executor/server.js index 8a0912bcc2..6f322f0892 100644 --- a/msa/js-executor/server.js +++ b/msa/js-executor/server.js @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +const { logLevel, Kafka } = require('kafkajs'); + const config = require('config'), - kafka = require('kafka-node'), - ConsumerGroup = kafka.ConsumerGroup, - Producer = kafka.Producer, JsInvokeMessageProcessor = require('./api/jsInvokeMessageProcessor'), - logger = require('./config/logger')('main'); + logger = require('./config/logger')._logger('main'), + KafkaJsWinstonLogCreator = require('./config/logger').KafkaJsWinstonLogCreator; var kafkaClient; +var consumer; +var producer; (async() => { try { @@ -32,49 +35,24 @@ var kafkaClient; logger.info('Kafka Bootstrap Servers: %s', kafkaBootstrapServers); logger.info('Kafka Requests Topic: %s', kafkaRequestTopic); - kafkaClient = new kafka.KafkaClient({kafkaHost: kafkaBootstrapServers}); - - var consumer = new ConsumerGroup( - { - kafkaHost: kafkaBootstrapServers, - groupId: 'js-executor-group', - autoCommit: true, - encoding: 'buffer' - }, - kafkaRequestTopic - ); - - consumer.on('error', (err) => { - logger.error('Unexpected kafka consumer error: %s', err.message); - logger.error(err.stack); - }); - - consumer.on('offsetOutOfRange', (err) => { - logger.error('Offset out of range error: %s', err.message); - logger.error(err.stack); - }); - - consumer.on('rebalancing', () => { - logger.info('Rebalancing event received.'); - }) - - consumer.on('rebalanced', () => { - logger.info('Rebalanced event received.'); - }); - - var producer = new Producer(kafkaClient); - producer.on('error', (err) => { - logger.error('Unexpected kafka producer error: %s', err.message); - logger.error(err.stack); + kafkaClient = new Kafka({ + brokers: kafkaBootstrapServers.split(','), + logLevel: logLevel.INFO, + logCreator: KafkaJsWinstonLogCreator }); - var messageProcessor = new JsInvokeMessageProcessor(producer); + consumer = kafkaClient.consumer({ groupId: 'js-executor-group' }); + producer = kafkaClient.producer(); + const messageProcessor = new JsInvokeMessageProcessor(producer); + await consumer.connect(); + await producer.connect(); + await consumer.subscribe({ topic: kafkaRequestTopic}); - producer.on('ready', () => { - consumer.on('message', (message) => { + logger.info('Started ThingsBoard JavaScript Executor Microservice.'); + await consumer.run({ + eachMessage: async ({ topic, partition, message }) => { messageProcessor.onJsInvokeMessage(message); - }); - logger.info('Started ThingsBoard JavaScript Executor Microservice.'); + }, }); } catch (e) { @@ -84,21 +62,41 @@ var kafkaClient; } })(); -process.on('exit', function () { +process.on('exit', () => { exit(0); }); -function exit(status) { +async function exit(status) { logger.info('Exiting with status: %d ...', status); - if (kafkaClient) { - logger.info('Stopping Kafka Client...'); - var _kafkaClient = kafkaClient; - kafkaClient = null; - _kafkaClient.close(() => { - logger.info('Kafka Client stopped.'); + if (consumer) { + logger.info('Stopping Kafka Consumer...'); + var _consumer = consumer; + consumer = null; + try { + await _consumer.disconnect(); + logger.info('Kafka Consumer stopped.'); + await disconnectProducer(); process.exit(status); - }); + } catch (e) { + logger.info('Kafka Consumer stop error.'); + await disconnectProducer(); + process.exit(status); + } } else { process.exit(status); } } + +async function disconnectProducer() { + if (producer) { + logger.info('Stopping Kafka Producer...'); + var _producer = producer; + producer = null; + try { + await _producer.disconnect(); + logger.info('Kafka Producer stopped.'); + } catch (e) { + logger.info('Kafka Producer stop error.'); + } + } +} diff --git a/msa/pom.xml b/msa/pom.xml index 84d00e2d88..5ea89c92f9 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 8a2bab084a..20f013a13a 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 680d39ede7..d0e78b334a 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 msa org.thingsboard.msa @@ -40,7 +40,7 @@ tb-cassandra thingsboard /usr/share/${pkg.name} - 2.4.0 + 2.4.1 diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index b606ba29fd..99c7d64423 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 2.4.2-SNAPSHOT + 2.4.2 transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index c38756d5cd..83ec9f5d94 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 2.4.2-SNAPSHOT + 2.4.2 transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 7402d2e209..aab05438f3 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 2.4.2-SNAPSHOT + 2.4.2 transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index f869e02f18..5a6defbb89 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 msa org.thingsboard.msa diff --git a/msa/web-ui/package-lock.json b/msa/web-ui/package-lock.json index 9298d31f27..3ceb92e809 100644 --- a/msa/web-ui/package-lock.json +++ b/msa/web-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "thingsboard-web-ui", - "version": "2.4.1", + "version": "2.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 2d36752300..ce82272a26 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "2.4.1", + "version": "2.4.2", "description": "ThingsBoard Web UI Microservice", "main": "server.js", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index c7bcf39caf..a55d816c33 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 869eb2abb0..349a154ed3 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,12 +19,12 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard org.thingsboard netty-mqtt - 2.4.2-SNAPSHOT + 2.4.2 jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index f0a0aae6cd..8dd4484e44 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 2.4.2-SNAPSHOT + 2.4.2 pom Thingsboard @@ -81,7 +81,7 @@ 2.5.0 2.5.3 1.2.1 - 9.4.1211 + 9.4.1212 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 4197f275b6..f40fce22a6 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 02e4d128dc..a24bebf8b1 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 rule-engine org.thingsboard.rule-engine @@ -48,6 +48,11 @@ dao-api provided + + org.thingsboard.common + util + provided + io.netty netty-all @@ -83,5 +88,10 @@ cassandra-driver-extras provided + + org.springframework.data + spring-data-redis + provided + - \ No newline at end of file + diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java index 4d9a4e612b..f72adf04ab 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.api; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.msg.TbMsg; import javax.script.ScriptException; @@ -25,10 +26,14 @@ public interface ScriptEngine { TbMsg executeUpdate(TbMsg msg) throws ScriptException; + ListenableFuture executeUpdateAsync(TbMsg msg); + TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException; boolean executeFilter(TbMsg msg) throws ScriptException; + ListenableFuture executeFilterAsync(TbMsg msg); + Set executeSwitch(TbMsg msg) throws ScriptException; JsonNode executeJson(TbMsg msg) throws ScriptException; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 034aec3000..d91f12da96 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -15,7 +15,10 @@ */ package org.thingsboard.rule.engine.api; +import com.datastax.driver.core.ResultSetFuture; import io.netty.channel.EventLoopGroup; +import org.springframework.data.redis.core.RedisTemplate; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.alarm.Alarm; @@ -41,8 +44,6 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; -import com.datastax.driver.core.ResultSetFuture; - import java.util.Set; /** @@ -122,6 +123,12 @@ public interface TbContext { ScriptEngine createJsScriptEngine(String script, String... argNames); + void logJsEvalRequest(); + + void logJsEvalResponse(); + + void logJsEvalFailure(); + String getNodeId(); RuleChainTransactionService getRuleChainTransactionService(); @@ -132,4 +139,7 @@ public interface TbContext { ResultSetFuture submitCassandraTask(CassandraStatementTask task); + RedisTemplate getRedisTemplate(); + + String getServerAddress(); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 627aa42ab3..69960df3b0 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index f295187170..0a7f6113f1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -65,15 +65,22 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); return Futures.transformAsync(asyncDetails, details -> { + ctx.logJsEvalResponse(); ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), details, System.currentTimeMillis()); return Futures.transformAsync(clearFuture, cleared -> { - if (cleared && details != null) { - alarm.setDetails(details); - } - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - return Futures.immediateFuture(new AlarmResult(false, false, true, alarm)); + ListenableFuture savedAlarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), alarm.getId()); + return Futures.transformAsync(savedAlarmFuture, savedAlarm -> { + if (cleared && savedAlarm != null) { + alarm.setDetails(savedAlarm.getDetails()); + alarm.setEndTs(savedAlarm.getEndTs()); + alarm.setClearTs(savedAlarm.getClearTs()); + } + alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); + return Futures.immediateFuture(new AlarmResult(false, false, true, alarm)); + }); }); }, ctx.getDbCallbackExecutor()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index dcf7b6987d..d6f6236311 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -42,10 +42,10 @@ import java.io.IOException; nodeDescription = "Create or Update Alarm", nodeDetails = "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + - "Node output:\n" + - "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm'. " + - "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + - "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", + "Node output:\n" + + "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm'. " + + "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeCreateAlarmConfig", icon = "notifications_active" @@ -103,11 +103,15 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) { ListenableFuture asyncAlarm; - if (msgAlarm != null ) { + if (msgAlarm != null) { asyncAlarm = Futures.immediateCheckedFuture(msgAlarm); } else { + ctx.logJsEvalRequest(); asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null), - details -> buildAlarm(msg, details, ctx.getTenantId())); + details -> { + ctx.logJsEvalResponse(); + return buildAlarm(msg, details, ctx.getTenantId()); + }); } ListenableFuture asyncCreated = Futures.transform(asyncAlarm, alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); @@ -115,7 +119,9 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) { + ctx.logJsEvalRequest(); ListenableFuture asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function) details -> { + ctx.logJsEvalResponse(); if (msgAlarm != null) { existingAlarm.setSeverity(msgAlarm.getSeverity()); existingAlarm.setPropagate(msgAlarm.isPropagate()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index 7a1578c0a1..f3fd84788c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.action; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -52,12 +53,17 @@ public class TbLogNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { ListeningExecutor jsExecutor = ctx.getJsExecutor(); + ctx.logJsEvalRequest(); withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)), toString -> { + ctx.logJsEvalResponse(); log.info(toString); ctx.tellNext(msg, SUCCESS); }, - t -> ctx.tellFailure(msg, t)); + t -> { + ctx.logJsEvalResponse(); + ctx.tellFailure(msg, t); + }); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 478b0b4666..d5ac552595 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -129,7 +129,9 @@ public class TbMsgGeneratorNode implements TbNode { prevMsg = ctx.newMsg("", originatorId, new TbMsgMetaData(), "{}"); } if (initialized) { + ctx.logJsEvalRequest(); TbMsg generated = jsEngine.executeGenerate(prevMsg); + ctx.logJsEvalResponse(); prevMsg = ctx.newMsg(generated.getType(), originatorId, generated.getMetaData(), generated.getData()); } return prevMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index afcf9dd80f..2effb714b9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -50,10 +51,16 @@ public class TbJsFilterNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ListeningExecutor jsExecutor = ctx.getJsExecutor(); - withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)), - filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"), - t -> ctx.tellFailure(msg, t)); + ctx.logJsEvalRequest(); + withCallback(jsEngine.executeFilterAsync(msg), + filterResult -> { + ctx.logJsEvalResponse(); + ctx.tellNext(msg, filterResult ? "True" : "False"); + }, + t -> { + ctx.tellFailure(msg, t); + ctx.logJsEvalFailure(); + }, ctx.getDbCallbackExecutor()); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index d8e25387b9..5ba4d2b0dd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -53,9 +54,16 @@ public class TbJsSwitchNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { ListeningExecutor jsExecutor = ctx.getJsExecutor(); + ctx.logJsEvalRequest(); withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)), - result -> processSwitch(ctx, msg, result), - t -> ctx.tellFailure(msg, t)); + result -> { + ctx.logJsEvalResponse(); + processSwitch(ctx, msg, result); + }, + t -> { + ctx.logJsEvalFailure(); + ctx.tellFailure(msg, t); + }, ctx.getDbCallbackExecutor()); } private void processSwitch(TbContext ctx, TbMsg msg, Set nextRelations) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index df6bcf9f58..7a26faa8f4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -15,6 +15,10 @@ */ package org.thingsboard.rule.engine.metadata; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.apache.commons.collections.CollectionUtils; @@ -40,11 +44,18 @@ import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; public abstract class TbAbstractGetAttributesNode implements TbNode { + private static ObjectMapper mapper = new ObjectMapper(); + + private static final String VALUE = "value"; + private static final String TS = "ts"; + protected C config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = loadGetAttributesNodeConfig(configuration); + mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false); + mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); } protected abstract C loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException; @@ -61,6 +72,11 @@ public abstract class TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg); + private void safePutAttributes(TbContext ctx, TbMsg msg, T entityId) { if (entityId == null || entityId.isNullUid()) { ctx.tellNext(msg, FAILURE); @@ -106,15 +122,22 @@ public abstract class TbAbstractGetAttributesNode> latest = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, keys); return Futures.transform(latest, l -> { l.forEach(r -> { + boolean getLatestValueWithTs = BooleanUtils.toBooleanDefaultIfNull(this.config.isGetLatestValueWithTs(), false); if (BooleanUtils.toBooleanDefaultIfNull(this.config.isTellFailureIfAbsent(), true)) { - if (r.getValue() != null) { - msg.getMetaData().putValue(r.getKey(), r.getValueAsString()); - } else { + if (r.getValue() == null) { throw new RuntimeException("[" + r.getKey() + "] telemetry value is not present in the DB!"); + } else if (getLatestValueWithTs) { + putValueWithTs(msg, r); + } else { + msg.getMetaData().putValue(r.getKey(), r.getValueAsString()); } } else { if (r.getValue() != null) { - msg.getMetaData().putValue(r.getKey(), r.getValueAsString()); + if (getLatestValueWithTs) { + putValueWithTs(msg, r); + } else { + msg.getMetaData().putValue(r.getKey(), r.getValueAsString()); + } } } }); @@ -122,10 +145,23 @@ public abstract class TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java index 2b47b5264a..a9a5c47ba0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java @@ -34,6 +34,7 @@ public class TbGetAttributesNodeConfiguration implements NodeConfiguration latestTsKeyNames; private boolean tellFailureIfAbsent; + private boolean getLatestValueWithTs; @Override public TbGetAttributesNodeConfiguration defaultConfiguration() { @@ -43,6 +44,7 @@ public class TbGetAttributesNodeConfiguration implements NodeConfiguration entries, TbMsg msg) { ObjectNode resultNode = mapper.createObjectNode(); - if (limit == MAX_FETCH_SIZE) { + if (FETCH_MODE_ALL.equals(fetchMode)) { entries.forEach(entry -> processArray(resultNode, entry)); } else { entries.forEach(entry -> processSingle(resultNode, entry)); @@ -156,12 +156,10 @@ public class TbGetTelemetryNode implements TbNode { private void processArray(ObjectNode node, TsKvEntry entry) { if (node.has(entry.getKey())) { ArrayNode arrayNode = (ArrayNode) node.get(entry.getKey()); - ObjectNode obj = buildNode(entry); - arrayNode.add(obj); + arrayNode.add(buildNode(entry)); } else { ArrayNode arrayNode = mapper.createArrayNode(); - ObjectNode obj = buildNode(entry); - arrayNode.add(obj); + arrayNode.add(buildNode(entry)); node.set(entry.getKey(), arrayNode); } } @@ -254,6 +252,14 @@ public class TbGetTelemetryNode implements TbNode { return pattern.replaceAll("[${}]", ""); } + private int validateLimit(int limit) { + if (limit != 0) { + return limit; + } else { + return MAX_FETCH_SIZE; + } + } + @Data @NoArgsConstructor private static class Interval { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java index 05335879ca..7a312c34fc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java @@ -45,7 +45,8 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration latestTsKeyNames; @@ -62,6 +63,7 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration>> pendingFutures; + + TbHttpClient(TbRestApiCallNodeConfiguration config) throws TbNodeException { + try { + this.config = config; + if (config.getMaxParallelRequestsCount() > 0) { + pendingFutures = new ConcurrentLinkedDeque<>(); + } + if (config.isUseSimpleClientHttpFactory()) { + httpClient = new AsyncRestTemplate(); + } else { + this.eventLoopGroup = new NioEventLoopGroup(); + Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup); + nettyFactory.setSslContext(SslContextBuilder.forClient().build()); + nettyFactory.setReadTimeout(config.getReadTimeoutMs()); + httpClient = new AsyncRestTemplate(nettyFactory); + } + } catch (SSLException e) { + throw new TbNodeException(e); + } + } + + void destroy() { + if (this.eventLoopGroup != null) { + this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); + } + } + + void processMessage(TbContext ctx, TbMsg msg, TbRedisQueueProcessor queueProcessor) { + String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData()); + HttpHeaders headers = prepareHeaders(msg.getMetaData()); + HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); + HttpEntity entity = new HttpEntity<>(msg.getData(), headers); + + ListenableFuture> future = httpClient.exchange( + endpointUrl, method, entity, String.class); + future.addCallback(new ListenableFutureCallback>() { + @Override + public void onFailure(Throwable throwable) { + if (config.isUseRedisQueueForMsgPersistence()) { + if (throwable instanceof HttpClientErrorException) { + processHttpClientError(((HttpClientErrorException) throwable).getStatusCode(), msg, queueProcessor); + } else { + queueProcessor.pushOnFailure(msg); + } + } + TbMsg next = processException(ctx, msg, throwable); + ctx.tellFailure(next, throwable); + } + + @Override + public void onSuccess(ResponseEntity responseEntity) { + if (responseEntity.getStatusCode().is2xxSuccessful()) { + if (config.isUseRedisQueueForMsgPersistence()) { + queueProcessor.resetCounter(); + } + TbMsg next = processResponse(ctx, msg, responseEntity); + ctx.tellNext(next, TbRelationTypes.SUCCESS); + } else { + if (config.isUseRedisQueueForMsgPersistence()) { + processHttpClientError(responseEntity.getStatusCode(), msg, queueProcessor); + } + TbMsg next = processFailureResponse(ctx, msg, responseEntity); + ctx.tellNext(next, TbRelationTypes.FAILURE); + } + } + }); + if (pendingFutures != null) { + processParallelRequests(future); + } + } + + private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { + TbMsgMetaData metaData = origMsg.getMetaData(); + metaData.putValue(STATUS, response.getStatusCode().name()); + metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); + metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); + response.getHeaders().toSingleValueMap().forEach(metaData::putValue); + return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, response.getBody()); + } + + private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { + TbMsgMetaData metaData = origMsg.getMetaData(); + metaData.putValue(STATUS, response.getStatusCode().name()); + metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); + metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); + metaData.putValue(ERROR_BODY, response.getBody()); + return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + } + + private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { + TbMsgMetaData metaData = origMsg.getMetaData(); + metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); + if (e instanceof HttpClientErrorException) { + HttpClientErrorException httpClientErrorException = (HttpClientErrorException) e; + metaData.putValue(STATUS, httpClientErrorException.getStatusText()); + metaData.putValue(STATUS_CODE, httpClientErrorException.getRawStatusCode() + ""); + metaData.putValue(ERROR_BODY, httpClientErrorException.getResponseBodyAsString()); + } + return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + } + + private HttpHeaders prepareHeaders(TbMsgMetaData metaData) { + HttpHeaders headers = new HttpHeaders(); + config.getHeaders().forEach((k, v) -> headers.add(TbNodeUtils.processPattern(k, metaData), TbNodeUtils.processPattern(v, metaData))); + return headers; + } + + private void processParallelRequests(ListenableFuture> future) { + pendingFutures.add(future); + if (pendingFutures.size() > config.getMaxParallelRequestsCount()) { + for (int i = 0; i < config.getMaxParallelRequestsCount(); i++) { + try { + ListenableFuture> pendingFuture = pendingFutures.removeFirst(); + try { + pendingFuture.get(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS); + } catch (Exception e) { + log.warn("Timeout during waiting for reply!", e); + pendingFuture.cancel(true); + } + } catch (Exception e) { + log.warn("Failure during waiting for reply!", e); + } + } + } + } + + private void processHttpClientError(HttpStatus statusCode, TbMsg msg, TbRedisQueueProcessor queueProcessor) { + if (statusCode.is4xxClientError()) { + log.warn("[{}] Client error during message delivering!", msg); + } else { + queueProcessor.pushOnFailure(msg); + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRedisQueueProcessor.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRedisQueueProcessor.java new file mode 100644 index 0000000000..7e10b469f2 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRedisQueueProcessor.java @@ -0,0 +1,125 @@ +/** + * 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.rule.engine.rest; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.ListOperations; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Data +@Slf4j +class TbRedisQueueProcessor { + + private static final int MAX_QUEUE_SIZE = Integer.MAX_VALUE; + + private final TbContext ctx; + private final TbHttpClient httpClient; + private final ExecutorService executor; + private final ListOperations listOperations; + private final String redisKey; + private final boolean trimQueue; + private final int maxQueueSize; + + private AtomicInteger failuresCounter; + private Future future; + + TbRedisQueueProcessor(TbContext ctx, TbHttpClient httpClient, boolean trimQueue, int maxQueueSize) { + this.ctx = ctx; + this.httpClient = httpClient; + this.executor = Executors.newSingleThreadExecutor(); + this.listOperations = ctx.getRedisTemplate().opsForList(); + this.redisKey = constructRedisKey(); + this.trimQueue = trimQueue; + this.maxQueueSize = maxQueueSize; + init(); + } + + private void init() { + failuresCounter = new AtomicInteger(0); + future = executor.submit(() -> { + while (true) { + if (failuresCounter.get() != 0 && failuresCounter.get() % 50 == 0) { + sleep("Target HTTP server is down...", 3); + } + if (listOperations.size(redisKey) > 0) { + List list = listOperations.range(redisKey, -10, -1); + list.forEach(obj -> { + TbMsg msg = TbMsg.fromBytes((byte[]) obj); + log.debug("Trying to send the message: {}", msg); + listOperations.remove(redisKey, -1, obj); + httpClient.processMessage(ctx, msg, this); + }); + } else { + sleep("Queue is empty, waiting for tasks!", 1); + } + } + }); + } + + void destroy() { + if (future != null) { + future.cancel(true); + } + if (executor != null) { + executor.shutdownNow(); + } + } + + void push(TbMsg msg) { + listOperations.leftPush(redisKey, TbMsg.toByteArray(msg)); + if (trimQueue) { + listOperations.trim(redisKey, 0, validateMaxQueueSize()); + } + } + + void pushOnFailure(TbMsg msg) { + listOperations.rightPush(redisKey, TbMsg.toByteArray(msg)); + failuresCounter.incrementAndGet(); + } + + void resetCounter() { + failuresCounter.set(0); + } + + private String constructRedisKey() { + return ctx.getServerAddress() + ctx.getSelfId(); + } + + private int validateMaxQueueSize() { + if (maxQueueSize != 0) { + return maxQueueSize; + } + return MAX_QUEUE_SIZE; + } + + private void sleep(String logMessage, int sleepSeconds) { + try { + log.debug(logMessage); + TimeUnit.SECONDS.sleep(sleepSeconds); + } catch (InterruptedException e) { + throw new IllegalStateException("Thread interrupted!", e); + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index e290c18d14..9fa65e775c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -15,28 +15,17 @@ */ package org.thingsboard.rule.engine.rest; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.handler.ssl.SslContextBuilder; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.Netty4ClientHttpRequestFactory; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.web.client.AsyncRestTemplate; -import org.springframework.web.client.HttpClientErrorException; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import javax.net.ssl.SSLException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; @Slf4j @RuleNode( @@ -56,107 +45,40 @@ import java.util.concurrent.TimeUnit; ) public class TbRestApiCallNode implements TbNode { - private static final String STATUS = "status"; - private static final String STATUS_CODE = "statusCode"; - private static final String STATUS_REASON = "statusReason"; - private static final String ERROR = "error"; - private static final String ERROR_BODY = "error_body"; - - private TbRestApiCallNodeConfiguration config; - - private EventLoopGroup eventLoopGroup; - private AsyncRestTemplate httpClient; + private boolean useRedisQueueForMsgPersistence; + private TbHttpClient httpClient; + private TbRedisQueueProcessor queueProcessor; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - try { - this.config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); - if (this.config.isUseSimpleClientHttpFactory()) { - httpClient = new AsyncRestTemplate(); - } else { - this.eventLoopGroup = new NioEventLoopGroup(); - Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup); - nettyFactory.setSslContext(SslContextBuilder.forClient().build()); - httpClient = new AsyncRestTemplate(nettyFactory); + TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); + httpClient = new TbHttpClient(config); + useRedisQueueForMsgPersistence = config.isUseRedisQueueForMsgPersistence(); + if (useRedisQueueForMsgPersistence) { + if (ctx.getRedisTemplate() == null) { + throw new RuntimeException("Redis cache type must be used!"); } - } catch (SSLException e) { - throw new TbNodeException(e); + queueProcessor = new TbRedisQueueProcessor(ctx, httpClient, config.isTrimQueue(), config.getMaxQueueSize()); } } @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { - String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData()); - HttpHeaders headers = prepareHeaders(msg.getMetaData()); - HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); - HttpEntity entity = new HttpEntity<>(msg.getData(), headers); - - ListenableFuture> future = httpClient.exchange( - endpointUrl, method, entity, String.class); - - future.addCallback(new ListenableFutureCallback>() { - @Override - public void onFailure(Throwable throwable) { - TbMsg next = processException(ctx, msg, throwable); - ctx.tellFailure(next, throwable); - } - - @Override - public void onSuccess(ResponseEntity responseEntity) { - if (responseEntity.getStatusCode().is2xxSuccessful()) { - TbMsg next = processResponse(ctx, msg, responseEntity); - ctx.tellNext(next, TbRelationTypes.SUCCESS); - } else { - TbMsg next = processFailureResponse(ctx, msg, responseEntity); - ctx.tellNext(next, TbRelationTypes.FAILURE); - } - } - }); + if (useRedisQueueForMsgPersistence) { + queueProcessor.push(msg); + } else { + httpClient.processMessage(ctx, msg, null); + } } @Override public void destroy() { - if (this.eventLoopGroup != null) { - this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); + if (this.httpClient != null) { + this.httpClient.destroy(); } - } - - private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { - TbMsgMetaData metaData = origMsg.getMetaData(); - metaData.putValue(STATUS, response.getStatusCode().name()); - metaData.putValue(STATUS_CODE, response.getStatusCode().value()+""); - metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); - response.getHeaders().toSingleValueMap().forEach(metaData::putValue); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, response.getBody()); - } - - private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { - TbMsgMetaData metaData = origMsg.getMetaData(); - metaData.putValue(STATUS, response.getStatusCode().name()); - metaData.putValue(STATUS_CODE, response.getStatusCode().value()+""); - metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); - metaData.putValue(ERROR_BODY, response.getBody()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); - } - - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { - TbMsgMetaData metaData = origMsg.getMetaData(); - metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); - if (e instanceof HttpClientErrorException) { - HttpClientErrorException httpClientErrorException = (HttpClientErrorException)e; - metaData.putValue(STATUS, httpClientErrorException.getStatusText()); - metaData.putValue(STATUS_CODE, httpClientErrorException.getRawStatusCode()+""); - metaData.putValue(ERROR_BODY, httpClientErrorException.getResponseBodyAsString()); + if (this.queueProcessor != null) { + this.queueProcessor.destroy(); } - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); - } - - private HttpHeaders prepareHeaders(TbMsgMetaData metaData) { - HttpHeaders headers = new HttpHeaders(); - config.getHeaders().forEach((k,v) -> { - headers.add(TbNodeUtils.processPattern(k, metaData), TbNodeUtils.processPattern(v, metaData)); - }); - return headers; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 6fa3f7151e..0cc3fe7aa2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -28,6 +28,11 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration headers; private boolean useSimpleClientHttpFactory; + private int readTimeoutMs; + private int maxParallelRequestsCount; + private boolean useRedisQueueForMsgPersistence; + private boolean trimQueue; + private int maxQueueSize; @Override public TbRestApiCallNodeConfiguration defaultConfiguration() { @@ -36,6 +41,10 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration { - if (m != null) { - ctx.tellNext(m, SUCCESS); - } else { - ctx.tellNext(msg, FAILURE); - } - }, - t -> ctx.tellFailure(msg, t)); + m -> transformSuccess(ctx, msg, m), + t -> transformFailure(ctx, msg, t), + ctx.getDbCallbackExecutor()); + } + + protected void transformFailure(TbContext ctx, TbMsg msg, Throwable t) { + ctx.tellFailure(msg, t); + } + + protected void transformSuccess(TbContext ctx, TbMsg msg, TbMsg m) { + if (m != null) { + ctx.tellNext(m, SUCCESS); + } else { + ctx.tellNext(msg, FAILURE); + } } protected abstract ListenableFuture transform(TbContext ctx, TbMsg msg); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 81c4483166..47e5b3c9c9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -21,6 +21,9 @@ import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; +import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; + @RuleNode( type = ComponentType.TRANSFORMATION, name = "script", @@ -49,7 +52,20 @@ public class TbTransformMsgNode extends TbAbstractTransformNode { @Override protected ListenableFuture transform(TbContext ctx, TbMsg msg) { - return ctx.getJsExecutor().executeAsync(() -> jsEngine.executeUpdate(msg)); + ctx.logJsEvalRequest(); + return jsEngine.executeUpdateAsync(msg); + } + + @Override + protected void transformSuccess(TbContext ctx, TbMsg msg, TbMsg m) { + ctx.logJsEvalResponse(); + super.transformSuccess(ctx, msg, m); + } + + @Override + protected void transformFailure(TbContext ctx, TbMsg msg, Throwable t) { + ctx.logJsEvalFailure(); + super.transformFailure(ctx, msg, t); } @Override diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index 0f252135e2..2ed9e929b1 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -1,6 +1,6 @@ -!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={exports:{},id:i,loaded:!1};return e[i].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),i=e[t[0]];return function(e,t,a){i.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(101)},function(e,t){},1,1,1,1,function(e,t){e.exports="
tb.rulenode.customer-name-pattern-required
tb.rulenode.customer-name-pattern-hint
{{ 'tb.rulenode.create-customer-if-not-exists' | translate }}
tb.rulenode.customer-cache-expiration-required
tb.rulenode.customer-cache-expiration-range
tb.rulenode.customer-cache-expiration-hint
"},function(e,t){e.exports='
{{scope.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
{{ 'tb.rulenode.use-message-alarm-data' | translate }}
tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
{{ severity.name | translate}}
tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }}
"},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
tb.rulenode.entity-name-pattern-required
tb.rulenode.entity-name-pattern-hint
tb.rulenode.entity-type-pattern-required
tb.rulenode.entity-type-pattern-hint
tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
{{ 'tb.rulenode.create-entity-if-not-exists' | translate }}
tb.rulenode.create-entity-if-not-exists-hint
{{ 'tb.rulenode.remove-current-relations' | translate }}
tb.rulenode.remove-current-relations-hint
{{ 'tb.rulenode.change-originator-to-related-entity' | translate }}
tb.rulenode.change-originator-to-related-entity-hint
tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports="
{{ 'tb.rulenode.delete-relation-to-specific-entity' | translate }}
tb.rulenode.delete-relation-hint
{{ ('relation.search-direction.' + direction) | translate}}
tb.rulenode.entity-name-pattern-required
tb.rulenode.entity-name-pattern-hint
tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports="
tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.latitude-key-name-required
tb.rulenode.longitude-key-name-required
{{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}
{{ type.name | translate}}
tb.rulenode.circle-center-latitude-required
tb.rulenode.circle-center-longitude-required
tb.rulenode.range-required
{{ type.name | translate}}
tb.rulenode.polygon-definition-required
tb.rulenode.polygon-definition-hint
tb.rulenode.min-inside-duration-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.min-outside-duration-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
'},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.bootstrap-servers-required
tb.rulenode.min-retries-message
tb.rulenode.min-batch-size-bytes-message
tb.rulenode.min-linger-ms-message
tb.rulenode.min-buffer-memory-bytes-message
{{ ackValue }}
tb.rulenode.key-serializer-required
tb.rulenode.value-serializer-required
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
tb.rulenode.connect-timeout-required
tb.rulenode.connect-timeout-range
tb.rulenode.connect-timeout-range
{{ \'tb.rulenode.clean-session\' | translate }} {{ \'tb.rulenode.enable-ssl\' | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.username-required
tb.rulenode.password-required
'},function(e,t){e.exports="
tb.rulenode.interval-seconds-required
tb.rulenode.min-interval-seconds-message
tb.rulenode.output-timeseries-key-prefix-required
"; -},function(e,t){e.exports='
{{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
tb.rulenode.period-seconds-required
tb.rulenode.min-period-0-seconds-message
tb.rulenode.period-in-seconds-pattern-required
tb.rulenode.period-in-seconds-pattern-hint
tb.rulenode.max-pending-messages-required
tb.rulenode.max-pending-messages-range
tb.rulenode.max-pending-messages-range
'},function(e,t){e.exports="
tb.rulenode.gcp-project-id-required
tb.rulenode.pubsub-topic-name-required
{{ 'action.remove' | translate }} close
tb.rulenode.message-attributes-hint
"},function(e,t){e.exports='
{{ property }}
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
{{ \'tb.rulenode.automatic-recovery\' | translate }}
tb.rulenode.min-connection-timeout-ms-message
tb.rulenode.min-handshake-timeout-ms-message
'},function(e,t){e.exports='
tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
{{ type }} {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}
tb.rulenode.headers-hint
'},function(e,t){e.exports="
"},function(e,t){e.exports="
tb.rulenode.timeout-required
tb.rulenode.min-timeout-message
"},function(e,t){e.exports='
tb.rulenode.custom-table-name-required
tb.rulenode.custom-table-hint
'},function(e,t){e.exports='
{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}
{{smtpProtocol.toUpperCase()}}
tb.rulenode.smtp-host-required
tb.rulenode.smtp-port-required
tb.rulenode.smtp-port-range
tb.rulenode.smtp-port-range
tb.rulenode.timeout-required
tb.rulenode.min-timeout-msec-message
{{ \'tb.rulenode.enable-tls\' | translate }}
'},function(e,t){e.exports="
tb.rulenode.topic-arn-pattern-required
tb.rulenode.topic-arn-pattern-hint
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
"},function(e,t){e.exports='
{{ type.name | translate }}
tb.rulenode.queue-url-pattern-required
tb.rulenode.queue-url-pattern-hint
tb.rulenode.min-delay-seconds-message
tb.rulenode.max-delay-seconds-message
tb.rulenode.message-attributes-hint
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
'},function(e,t){e.exports="
tb.rulenode.default-ttl-required
tb.rulenode.min-default-ttl-message
"},function(e,t){e.exports="
tb.rulenode.customer-name-pattern-required
tb.rulenode.customer-name-pattern-hint
tb.rulenode.customer-cache-expiration-required
tb.rulenode.customer-cache-expiration-range
tb.rulenode.customer-cache-expiration-hint
"},function(e,t){e.exports='
{{ (\'relation.search-direction.\' + direction) | translate}}
relation.relation-type
device.device-types
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},function(e,t){e.exports='
{{ \'tb.rulenode.tell-failure-if-absent\' | translate }}
tb.rulenode.tell-failure-if-absent-hint
'},function(e,t){e.exports='
{{\'tb.rulenode.entity-details-\'+item.toLowerCase() | translate}} tb.rulenode.no-entity-details-matching {{\'tb.rulenode.entity-details-\'+$chip.toLowerCase() | translate}} {{ \'tb.rulenode.add-to-metadata\' | translate }}
tb.rulenode.add-to-metadata-hint
'},function(e,t){e.exports='
{{ type }}
tb.rulenode.fetch-mode-hint
{{ type }}
tb.rulenode.order-by-hint
{{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}
tb.rulenode.use-metadata-interval-patterns-hint
tb.rulenode.start-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.end-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.start-interval-pattern-required
tb.rulenode.start-interval-pattern-hint
tb.rulenode.end-interval-pattern-required
tb.rulenode.end-interval-pattern-hint
'},function(e,t){e.exports='
{{ \'tb.rulenode.tell-failure-if-absent\' | translate }}
tb.rulenode.tell-failure-if-absent-hint
'},function(e,t){e.exports='
'; -},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},31,function(e,t){e.exports='
tb.rulenode.separator-hint
tb.rulenode.separator-hint
{{ \'tb.rulenode.check-all-keys\' | translate }}
tb.rulenode.check-all-keys-hint
'},function(e,t){e.exports="
{{ 'tb.rulenode.check-relation-to-specific-entity' | translate }}
tb.rulenode.check-relation-hint
{{ ('relation.search-direction.' + direction) | translate}}
"},function(e,t){e.exports='
tb.rulenode.latitude-key-name-required
tb.rulenode.longitude-key-name-required
{{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}
{{ type.name | translate}}
tb.rulenode.circle-center-latitude-required
tb.rulenode.circle-center-longitude-required
tb.rulenode.range-required
{{ type.name | translate}}
tb.rulenode.polygon-definition-required
tb.rulenode.polygon-definition-hint
'},function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports='
{{ keyText }} {{ valText }}  
{{keyRequiredText}}
{{valRequiredText}}
{{ \'tb.key-val.remove-entry\' | translate }} close
{{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-filters
"},function(e,t){e.exports='
{{ source.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports="
tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(6),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(7),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(8),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(9),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(10),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(11),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript);i.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var r=n(12),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(13),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(74),r=i(a),o=n(52),l=i(o),s=n(57),d=i(s),u=n(54),c=i(u),m=n(53),g=i(m),p=n(61),f=i(p),b=n(68),v=i(b),y=n(69),h=i(y),q=n(67),$=i(q),x=n(60),k=i(x),T=n(72),C=i(T),w=n(73),M=i(w),N=n(66),_=i(N),S=n(62),E=i(S),F=n(71),P=i(F),A=n(64),V=i(A),I=n(63),j=i(I),O=n(51),D=i(O),R=n(75),K=i(R),L=n(56),U=i(L),z=n(55),B=i(z),H=n(70),G=i(H),Y=n(58),Q=i(Y),J=n(65),W=i(J);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",r.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",d.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",h.default).directive("tbActionNodeRestApiCallConfig",$.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",_.default).directive("tbActionNodeMqttConfig",E.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",V.default).directive("tbActionNodeMsgCountConfig",j.default).directive("tbActionNodeAssignToCustomerConfig",D.default).directive("tbActionNodeUnAssignToCustomerConfig",K.default).directive("tbActionNodeDeleteRelationConfig",U.default).directive("tbActionNodeCreateRelationConfig",B.default).directive("tbActionNodeCustomTableConfig",G.default).directive("tbActionNodeGpsGeofencingConfig",Q.default).directive("tbActionNodePubSubConfig",W.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(14),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(15),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$mdExpansionPanel=t,i.ruleNodeTypes=n,i.credentialsTypeChanged=function(){var e=i.configuration.credentials.type;i.configuration.credentials={},i.configuration.credentials.type=e,i.updateValidity()},i.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){i.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(i.configuration.credentials.caCertFileName=e.name,i.configuration.credentials.caCert=a),"privateKey"==t&&(i.configuration.credentials.privateKeyFileName=e.name,i.configuration.credentials.privateKey=a),"Cert"==t&&(i.configuration.credentials.certFileName=e.name,i.configuration.credentials.cert=a)),i.updateValidity()}})},n.readAsText(e.file)},i.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(i.configuration.credentials.caCertFileName=null,i.configuration.credentials.caCert=null),"privateKey"==e&&(i.configuration.credentials.privateKeyFileName=null,i.configuration.credentials.privateKey=null),"Cert"==e&&(i.configuration.credentials.certFileName=null,i.configuration.credentials.cert=null),i.updateValidity()},i.updateValidity=function(){var e=!0,t=i.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:i}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var r=n(16),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(17),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(18),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.serviceAccountFileAdded=function(e){var t=new FileReader;t.onload=function(t){n.$apply(function(){if(t.target.result){r.$setDirty();var i=t.target.result;i&&i.length>0&&(n.configuration.serviceAccountKeyFileName=e.name,n.configuration.serviceAccountKey=i),n.updateValidity()}})},t.readAsText(e.file)},n.clearServiceAccountFile=function(){r.$setDirty(),n.configuration.serviceAccountKeyFileName=null,n.configuration.serviceAccountKey=null,n.updateValidity()},n.updateValidity=function(){var e=!0,t=n.configuration;t.serviceAccountKeyFileName&&t.serviceAccountKey||(e=!1),r.$setValidity("SAKey",e)},n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(19),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(20),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(21),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(22),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(23),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(24),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly" -},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(25),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(26),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(27),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(28),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(29),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(30),o=i(r)},function(e,t){"use strict";function n(e){var t=function(t,n,i,a){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(31),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(32),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),n.entityDetailsList=[];for(var s in t.entityDetails){var d=s;n.entityDetailsList.push(d)}r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(33),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s);var d=186;i.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,d],i.ruleNodeTypes=n,i.aggPeriodTimeUnits={},i.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,i.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,i.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,i.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,i.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{},link:i}}a.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(34),o=i(r);n(3)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(83),r=i(a),o=n(84),l=i(o),s=n(79),d=i(s),u=n(85),c=i(u),m=n(78),g=i(m),p=n(86),f=i(p),b=n(81),v=i(b),y=n(80),h=i(y);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",r.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",d.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).directive("tbEnrichmentNodeEntityDetailsConfig",h.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(35),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(36),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(37),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(38),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(39),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(40),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(41),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(93),r=i(a),o=n(91),l=i(o),s=n(94),d=i(s),u=n(88),c=i(u),m=n(92),g=i(m),p=n(87),f=i(p),b=n(89),v=i(b);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",r.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",d.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).directive("tbFilterNodeCheckMessageConfig",f.default).directive("tbFilterNodeGpsGeofencingConfig",v.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),d()}function d(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var u=o.default;n.html(u),t.ngModelCtrl=a,t.removeKeyVal=r,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),d()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(46),o=i(r);n(5)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(47),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(48),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(97),r=i(a),o=n(99),l=i(o),s=n(100),d=i(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",r.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",d.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(49),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(50),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(104),r=i(a),o=n(90),l=i(o),s=n(82),d=i(s),u=n(98),c=i(u),m=n(59),g=i(m),p=n(77),f=i(p),b=n(96),v=i(b),y=n(76),h=i(y),q=n(95),$=i(q),x=n(103),k=i(x);t.default=angular.module("thingsboard.ruleChain.config",[r.default,l.default,d.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",h.default).directive("tbKvMapConfig",$.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use ${metaKeyName} to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use ${metaKeyName} to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use ${metaKeyName} to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647'.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use metadata period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds metadata pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","period-in-seconds-pattern-hint":"Period in seconds pattern, use ${metaKeyName} to substitute variables from metadata","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory",headers:"Headers","headers-hint":"Use ${metaKeyName} in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use ${metaKeyName} to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use ${metaKeyName} to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":"Use ${metaKeyName} in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.", -"start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use ${metaKeyName} to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use ${metaKeyName} to substitute variables from metadata","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};e.translations("en_US",t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){(0,o.default)(e)}a.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(102),o=i(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"},ALARM_ORIGINATOR:{name:"tb.rulenode.originator-alarm-originator",value:"ALARM_ORIGINATOR"}},fetchModeType:["FIRST","LAST","ALL"],samplingOrder:["ASC","DESC"],httpRequestType:["GET","POST","PUT","DELETE"],entityDetails:{TITLE:{name:"tb.rulenode.entity-details-title",value:"TITLE"},COUNTRY:{name:"tb.rulenode.entity-details-country",value:"COUNTRY"},STATE:{name:"tb.rulenode.entity-details-state",value:"STATE"},ZIP:{name:"tb.rulenode.entity-details-zip",value:"ZIP"},ADDRESS:{name:"tb.rulenode.entity-details-address",value:"ADDRESS"},ADDRESS2:{name:"tb.rulenode.entity-details-address2",value:"ADDRESS2"},PHONE:{name:"tb.rulenode.entity-details-phone",value:"PHONE"},EMAIL:{name:"tb.rulenode.entity-details-email",value:"EMAIL"},ADDITIONAL_INFO:{name:"tb.rulenode.entity-details-additional_info",value:"ADDITIONAL_INFO"}},sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},perimeterType:{CIRCLE:{name:"tb.rulenode.perimeter-circle",value:"CIRCLE"},POLYGON:{name:"tb.rulenode.perimeter-polygon",value:"POLYGON"}},timeUnit:{MILLISECONDS:{value:"MILLISECONDS",name:"tb.rulenode.time-unit-milliseconds"},SECONDS:{value:"SECONDS",name:"tb.rulenode.time-unit-seconds"},MINUTES:{value:"MINUTES",name:"tb.rulenode.time-unit-minutes"},HOURS:{value:"HOURS",name:"tb.rulenode.time-unit-hours"},DAYS:{value:"DAYS",name:"tb.rulenode.time-unit-days"}},rangeUnit:{METER:{value:"METER",name:"tb.rulenode.range-unit-meter"},KILOMETER:{value:"KILOMETER",name:"tb.rulenode.range-unit-kilometer"},FOOT:{value:"FOOT",name:"tb.rulenode.range-unit-foot"},MILE:{value:"MILE",name:"tb.rulenode.range-unit-mile"},NAUTICAL_MILE:{value:"NAUTICAL_MILE",name:"tb.rulenode.range-unit-nautical-mile"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}])); +!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={exports:{},id:i,loaded:!1};return e[i].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),i=e[t[0]];return function(e,t,a){i.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(101)},function(e,t){},1,1,1,1,function(e,t){e.exports="
tb.rulenode.customer-name-pattern-required
tb.rulenode.customer-name-pattern-hint
{{ 'tb.rulenode.create-customer-if-not-exists' | translate }}
tb.rulenode.customer-cache-expiration-required
tb.rulenode.customer-cache-expiration-range
tb.rulenode.customer-cache-expiration-hint
"},function(e,t){e.exports='
{{scope.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
{{ 'tb.rulenode.use-message-alarm-data' | translate }}
tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
{{ severity.name | translate}}
tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }}
"},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
tb.rulenode.entity-name-pattern-required
tb.rulenode.entity-name-pattern-hint
tb.rulenode.entity-type-pattern-required
tb.rulenode.entity-type-pattern-hint
tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
{{ 'tb.rulenode.create-entity-if-not-exists' | translate }}
tb.rulenode.create-entity-if-not-exists-hint
{{ 'tb.rulenode.remove-current-relations' | translate }}
tb.rulenode.remove-current-relations-hint
{{ 'tb.rulenode.change-originator-to-related-entity' | translate }}
tb.rulenode.change-originator-to-related-entity-hint
tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports="
{{ 'tb.rulenode.delete-relation-to-specific-entity' | translate }}
tb.rulenode.delete-relation-hint
{{ ('relation.search-direction.' + direction) | translate}}
tb.rulenode.entity-name-pattern-required
tb.rulenode.entity-name-pattern-hint
tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports="
tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.latitude-key-name-required
tb.rulenode.longitude-key-name-required
{{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}
{{ type.name | translate}}
tb.rulenode.circle-center-latitude-required
tb.rulenode.circle-center-longitude-required
tb.rulenode.range-required
{{ type.name | translate}}
tb.rulenode.polygon-definition-required
tb.rulenode.polygon-definition-hint
tb.rulenode.min-inside-duration-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.min-outside-duration-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
'},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.bootstrap-servers-required
tb.rulenode.min-retries-message
tb.rulenode.min-batch-size-bytes-message
tb.rulenode.min-linger-ms-message
tb.rulenode.min-buffer-memory-bytes-message
{{ ackValue }}
tb.rulenode.key-serializer-required
tb.rulenode.value-serializer-required
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
tb.rulenode.connect-timeout-required
tb.rulenode.connect-timeout-range
tb.rulenode.connect-timeout-range
{{ \'tb.rulenode.clean-session\' | translate }} {{ \'tb.rulenode.enable-ssl\' | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.username-required
tb.rulenode.password-required
'},function(e,t){e.exports="
tb.rulenode.interval-seconds-required
tb.rulenode.min-interval-seconds-message
tb.rulenode.output-timeseries-key-prefix-required
"; +},function(e,t){e.exports='
{{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
tb.rulenode.period-seconds-required
tb.rulenode.min-period-0-seconds-message
tb.rulenode.period-in-seconds-pattern-required
tb.rulenode.period-in-seconds-pattern-hint
tb.rulenode.max-pending-messages-required
tb.rulenode.max-pending-messages-range
tb.rulenode.max-pending-messages-range
'},function(e,t){e.exports="
tb.rulenode.gcp-project-id-required
tb.rulenode.pubsub-topic-name-required
{{ 'action.remove' | translate }} close
tb.rulenode.message-attributes-hint
"},function(e,t){e.exports='
{{ property }}
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
{{ \'tb.rulenode.automatic-recovery\' | translate }}
tb.rulenode.min-connection-timeout-ms-message
tb.rulenode.min-handshake-timeout-ms-message
'},function(e,t){e.exports='
tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
{{ type }} {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}
tb.rulenode.read-timeout-hint
tb.rulenode.max-parallel-requests-count-hint
tb.rulenode.headers-hint
{{ \'tb.rulenode.use-redis-queue\' | translate }}
{{ \'tb.rulenode.trim-redis-queue\' | translate }}
'},function(e,t){e.exports="
"},function(e,t){e.exports="
tb.rulenode.timeout-required
tb.rulenode.min-timeout-message
"},function(e,t){e.exports='
tb.rulenode.custom-table-name-required
tb.rulenode.custom-table-hint
'},function(e,t){e.exports='
{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}
{{smtpProtocol.toUpperCase()}}
tb.rulenode.smtp-host-required
tb.rulenode.smtp-port-required
tb.rulenode.smtp-port-range
tb.rulenode.smtp-port-range
tb.rulenode.timeout-required
tb.rulenode.min-timeout-msec-message
{{ \'tb.rulenode.enable-tls\' | translate }}
'},function(e,t){e.exports="
tb.rulenode.topic-arn-pattern-required
tb.rulenode.topic-arn-pattern-hint
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
"},function(e,t){e.exports='
{{ type.name | translate }}
tb.rulenode.queue-url-pattern-required
tb.rulenode.queue-url-pattern-hint
tb.rulenode.min-delay-seconds-message
tb.rulenode.max-delay-seconds-message
tb.rulenode.message-attributes-hint
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
'},function(e,t){e.exports="
tb.rulenode.default-ttl-required
tb.rulenode.min-default-ttl-message
"},function(e,t){e.exports="
tb.rulenode.customer-name-pattern-required
tb.rulenode.customer-name-pattern-hint
tb.rulenode.customer-cache-expiration-required
tb.rulenode.customer-cache-expiration-range
tb.rulenode.customer-cache-expiration-hint
"},function(e,t){e.exports='
{{ (\'relation.search-direction.\' + direction) | translate}}
relation.relation-type
device.device-types
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},function(e,t){e.exports='
{{ \'tb.rulenode.tell-failure-if-absent\' | translate }}
tb.rulenode.tell-failure-if-absent-hint
{{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}
tb.rulenode.get-latest-value-with-ts-hint
'},function(e,t){e.exports='
{{\'tb.rulenode.entity-details-\'+item.toLowerCase() | translate}} tb.rulenode.no-entity-details-matching {{\'tb.rulenode.entity-details-\'+$chip.toLowerCase() | translate}} {{ \'tb.rulenode.add-to-metadata\' | translate }}
tb.rulenode.add-to-metadata-hint
'},function(e,t){e.exports='
{{ type }}
tb.rulenode.fetch-mode-hint
{{ type }}
tb.rulenode.order-by-hint
tb.rulenode.limit-hint
{{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}
tb.rulenode.use-metadata-interval-patterns-hint
tb.rulenode.start-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.end-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
{{timeUnit.name | translate}}
tb.rulenode.start-interval-pattern-required
tb.rulenode.start-interval-pattern-hint
tb.rulenode.end-interval-pattern-required
tb.rulenode.end-interval-pattern-hint
'; +},function(e,t){e.exports='
{{ \'tb.rulenode.tell-failure-if-absent\' | translate }}
tb.rulenode.tell-failure-if-absent-hint
{{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}
tb.rulenode.get-latest-value-with-ts-hint
'},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},31,function(e,t){e.exports='
tb.rulenode.separator-hint
tb.rulenode.separator-hint
{{ \'tb.rulenode.check-all-keys\' | translate }}
tb.rulenode.check-all-keys-hint
'},function(e,t){e.exports="
{{ 'tb.rulenode.check-relation-to-specific-entity' | translate }}
tb.rulenode.check-relation-hint
{{ ('relation.search-direction.' + direction) | translate}}
"},function(e,t){e.exports='
tb.rulenode.latitude-key-name-required
tb.rulenode.longitude-key-name-required
{{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}
{{ type.name | translate}}
tb.rulenode.circle-center-latitude-required
tb.rulenode.circle-center-longitude-required
tb.rulenode.range-required
{{ type.name | translate}}
tb.rulenode.polygon-definition-required
tb.rulenode.polygon-definition-hint
'},function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports='
{{ keyText }} {{ valText }}  
{{keyRequiredText}}
{{valRequiredText}}
{{ \'tb.key-val.remove-entry\' | translate }} close
{{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-filters
"},function(e,t){e.exports='
{{ source.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports="
tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(6),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(7),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(8),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(9),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(10),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(11),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript);i.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var r=n(12),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(13),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(74),r=i(a),o=n(52),l=i(o),s=n(57),d=i(s),u=n(54),c=i(u),m=n(53),g=i(m),p=n(61),f=i(p),b=n(68),v=i(b),y=n(69),h=i(y),q=n(67),x=i(q),k=n(60),$=i(k),T=n(72),C=i(T),w=n(73),M=i(w),N=n(66),S=i(N),_=n(62),E=i(_),F=n(71),P=i(F),A=n(64),V=i(A),I=n(63),j=i(I),O=n(51),D=i(O),R=n(75),K=i(R),L=n(56),U=i(L),z=n(55),H=i(z),B=n(70),G=i(B),Y=n(58),Q=i(Y),W=n(65),J=i(W);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",r.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",d.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",h.default).directive("tbActionNodeRestApiCallConfig",x.default).directive("tbActionNodeKafkaConfig",$.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",E.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",V.default).directive("tbActionNodeMsgCountConfig",j.default).directive("tbActionNodeAssignToCustomerConfig",D.default).directive("tbActionNodeUnAssignToCustomerConfig",K.default).directive("tbActionNodeDeleteRelationConfig",U.default).directive("tbActionNodeCreateRelationConfig",H.default).directive("tbActionNodeCustomTableConfig",G.default).directive("tbActionNodeGpsGeofencingConfig",Q.default).directive("tbActionNodePubSubConfig",J.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(14),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(15),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$mdExpansionPanel=t,i.ruleNodeTypes=n,i.credentialsTypeChanged=function(){var e=i.configuration.credentials.type;i.configuration.credentials={},i.configuration.credentials.type=e,i.updateValidity()},i.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){i.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(i.configuration.credentials.caCertFileName=e.name,i.configuration.credentials.caCert=a),"privateKey"==t&&(i.configuration.credentials.privateKeyFileName=e.name,i.configuration.credentials.privateKey=a),"Cert"==t&&(i.configuration.credentials.certFileName=e.name,i.configuration.credentials.cert=a)),i.updateValidity()}})},n.readAsText(e.file)},i.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(i.configuration.credentials.caCertFileName=null,i.configuration.credentials.caCert=null),"privateKey"==e&&(i.configuration.credentials.privateKeyFileName=null,i.configuration.credentials.privateKey=null),"Cert"==e&&(i.configuration.credentials.certFileName=null,i.configuration.credentials.cert=null),i.updateValidity()},i.updateValidity=function(){var e=!0,t=i.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:i}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var r=n(16),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(17),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(18),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.serviceAccountFileAdded=function(e){var t=new FileReader;t.onload=function(t){n.$apply(function(){if(t.target.result){r.$setDirty();var i=t.target.result;i&&i.length>0&&(n.configuration.serviceAccountKeyFileName=e.name,n.configuration.serviceAccountKey=i),n.updateValidity()}})},t.readAsText(e.file)},n.clearServiceAccountFile=function(){r.$setDirty(),n.configuration.serviceAccountKeyFileName=null,n.configuration.serviceAccountKey=null,n.updateValidity()},n.updateValidity=function(){var e=!0,t=n.configuration;t.serviceAccountKeyFileName&&t.serviceAccountKey||(e=!1),r.$setValidity("SAKey",e)},n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(19),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"], +Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(20),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(21),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(22),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(23),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(24),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(25),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(26),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(27),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(28),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(29),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(30),o=i(r)},function(e,t){"use strict";function n(e){var t=function(t,n,i,a){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(31),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(32),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),n.entityDetailsList=[];for(var s in t.entityDetails){var d=s;n.entityDetailsList.push(d)}r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(33),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s);var d=186;i.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,d],i.ruleNodeTypes=n,i.aggPeriodTimeUnits={},i.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,i.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,i.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,i.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,i.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{},link:i}}a.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(34),o=i(r);n(3)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(83),r=i(a),o=n(84),l=i(o),s=n(79),d=i(s),u=n(85),c=i(u),m=n(78),g=i(m),p=n(86),f=i(p),b=n(81),v=i(b),y=n(80),h=i(y);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",r.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",d.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).directive("tbEnrichmentNodeEntityDetailsConfig",h.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(35),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(36),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(37),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(38),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(39),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(40),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(41),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(93),r=i(a),o=n(91),l=i(o),s=n(94),d=i(s),u=n(88),c=i(u),m=n(92),g=i(m),p=n(87),f=i(p),b=n(89),v=i(b);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",r.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",d.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).directive("tbFilterNodeCheckMessageConfig",f.default).directive("tbFilterNodeGpsGeofencingConfig",v.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),d()}function d(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var u=o.default;n.html(u),t.ngModelCtrl=a,t.removeKeyVal=r,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),d()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(46),o=i(r);n(5)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(47),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(48),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(97),r=i(a),o=n(99),l=i(o),s=n(100),d=i(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",r.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",d.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(49),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(50),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(104),r=i(a),o=n(90),l=i(o),s=n(82),d=i(s),u=n(98),c=i(u),m=n(59),g=i(m),p=n(77),f=i(p),b=n(96),v=i(b),y=n(76),h=i(y),q=n(95),x=i(q),k=n(103),$=i(k);t.default=angular.module("thingsboard.ruleChain.config",[r.default,l.default,d.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",h.default).directive("tbKvMapConfig",x.default).config($.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use ${metaKeyName} to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use ${metaKeyName} to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use ${metaKeyName} to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647'.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use metadata period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds metadata pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","period-in-seconds-pattern-hint":"Period in seconds pattern, use ${metaKeyName} to substitute variables from metadata","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":"Use ${metaKeyName} in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use ${metaKeyName} to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use ${metaKeyName} to substitute variables from metadata", +"delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":"Use ${metaKeyName} in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use ${metaKeyName} to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use ${metaKeyName} to substitute variables from metadata","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};e.translations("en_US",t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){(0,o.default)(e)}a.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(102),o=i(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"},ALARM_ORIGINATOR:{name:"tb.rulenode.originator-alarm-originator",value:"ALARM_ORIGINATOR"}},fetchModeType:["FIRST","LAST","ALL"],samplingOrder:["ASC","DESC"],httpRequestType:["GET","POST","PUT","DELETE"],entityDetails:{TITLE:{name:"tb.rulenode.entity-details-title",value:"TITLE"},COUNTRY:{name:"tb.rulenode.entity-details-country",value:"COUNTRY"},STATE:{name:"tb.rulenode.entity-details-state",value:"STATE"},ZIP:{name:"tb.rulenode.entity-details-zip",value:"ZIP"},ADDRESS:{name:"tb.rulenode.entity-details-address",value:"ADDRESS"},ADDRESS2:{name:"tb.rulenode.entity-details-address2",value:"ADDRESS2"},PHONE:{name:"tb.rulenode.entity-details-phone",value:"PHONE"},EMAIL:{name:"tb.rulenode.entity-details-email",value:"EMAIL"},ADDITIONAL_INFO:{name:"tb.rulenode.entity-details-additional_info",value:"ADDITIONAL_INFO"}},sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},perimeterType:{CIRCLE:{name:"tb.rulenode.perimeter-circle",value:"CIRCLE"},POLYGON:{name:"tb.rulenode.perimeter-polygon",value:"POLYGON"}},timeUnit:{MILLISECONDS:{value:"MILLISECONDS",name:"tb.rulenode.time-unit-milliseconds"},SECONDS:{value:"SECONDS",name:"tb.rulenode.time-unit-seconds"},MINUTES:{value:"MINUTES",name:"tb.rulenode.time-unit-minutes"},HOURS:{value:"HOURS",name:"tb.rulenode.time-unit-hours"},DAYS:{value:"DAYS",name:"tb.rulenode.time-unit-days"}},rangeUnit:{METER:{value:"METER",name:"tb.rulenode.range-unit-meter"},KILOMETER:{value:"KILOMETER",name:"tb.rulenode.range-unit-kilometer"},FOOT:{value:"FOOT",name:"tb.rulenode.range-unit-foot"},MILE:{value:"MILE",name:"tb.rulenode.range-unit-mile"},NAUTICAL_MILE:{value:"NAUTICAL_MILE",name:"tb.rulenode.range-unit-nautical-mile"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}])); //# sourceMappingURL=rulenode-core-config.js.map \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index 32318edcb8..fb07951368 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -28,6 +28,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.DeviceId; @@ -156,6 +157,7 @@ public class TbAlarmNodeTest { verify(ctx, times(1)).getJsExecutor(); verify(ctx).getAlarmService(); verify(ctx, times(3)).getDbCallbackExecutor(); + verify(ctx).logJsEvalRequest(); verify(ctx).getTenantId(); verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType"); @@ -270,6 +272,7 @@ public class TbAlarmNodeTest { // when(detailsJs.executeJson(msg)).thenReturn(null); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true)); + when(alarmService.findAlarmByIdAsync(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()))).thenReturn(Futures.immediateFuture(activeAlarm)); // doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); node.onMsg(ctx, msg); @@ -375,4 +378,4 @@ public class TbAlarmNodeTest { assertEquals(message, value.getMessage()); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index f1aace48bb..d30a2f8626 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -19,6 +19,7 @@ import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -26,6 +27,7 @@ import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -58,10 +60,10 @@ public class TbJsFilterNodeTest { initWithScript(); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L); mockJsExecutor(); - when(scriptEngine.executeFilter(msg)).thenReturn(false); + when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(false)); node.onMsg(ctx, msg); - verify(ctx).getJsExecutor(); + verify(ctx).getDbCallbackExecutor(); verify(ctx).tellNext(msg, "False"); } @@ -71,7 +73,7 @@ public class TbJsFilterNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L); mockJsExecutor(); - when(scriptEngine.executeFilter(msg)).thenThrow(new ScriptException("error")); + when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFailedFuture(new ScriptException("error"))); node.onMsg(ctx, msg); @@ -84,10 +86,10 @@ public class TbJsFilterNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L); mockJsExecutor(); - when(scriptEngine.executeFilter(msg)).thenReturn(true); + when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); - verify(ctx).getJsExecutor(); + verify(ctx).getDbCallbackExecutor(); verify(ctx).tellNext(msg, "True"); } @@ -123,4 +125,4 @@ public class TbJsFilterNodeTest { assertEquals(expectedClass, value.getClass()); assertEquals(message, value.getMessage()); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java index 82ecbb4ba8..eca1a4a684 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java @@ -27,6 +27,7 @@ import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -105,4 +106,4 @@ public class TbJsSwitchNodeTest { assertEquals(expectedClass, value.getClass()); assertEquals(message, value.getMessage()); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index feb561bb97..d31d184127 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -25,7 +25,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import org.thingsboard.rule.engine.api.ListeningExecutor; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -166,4 +166,4 @@ public class TbChangeOriginatorNodeTest { node = new TbChangeOriginatorNode(); node.init(null, nodeConfiguration); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index 7f577255da..f6464caf15 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -26,6 +26,7 @@ import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -64,10 +65,10 @@ public class TbTransformMsgNodeTest { TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L); TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}", ruleChainId, ruleNodeId, 0L); mockJsExecutor(); - when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg); + when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFuture(transformedMsg)); node.onMsg(ctx, msg); - verify(ctx).getJsExecutor(); + verify(ctx).getDbCallbackExecutor(); ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).tellNext(captor.capture(), eq(SUCCESS)); TbMsg actualMsg = captor.getValue(); @@ -85,7 +86,7 @@ public class TbTransformMsgNodeTest { RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased()); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L); mockJsExecutor(); - when(scriptEngine.executeUpdate(msg)).thenThrow(new IllegalStateException("error")); + when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("error"))); node.onMsg(ctx, msg); verifyError(msg, "error", IllegalStateException.class); @@ -123,4 +124,4 @@ public class TbTransformMsgNodeTest { assertEquals(expectedClass, value.getClass()); assertEquals(message, value.getMessage()); } -} \ No newline at end of file +} diff --git a/tools/pom.xml b/tools/pom.xml index 88f8ccdacb..cc215257dd 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard tools @@ -51,6 +51,39 @@ com.google.guava guava + + org.apache.cassandra + cassandra-all + 3.11.4 + + + com.datastax.cassandra + cassandra-driver-core + + + commons-io + commons-io + + + + + + maven-assembly-plugin + + + + org.thingsboard.client.tools.migrator.MigratorTool + + + + jar-with-dependencies + + + + + + + diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java index 44f345dc96..97ce718cc8 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java +++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java @@ -16,7 +16,11 @@ package org.thingsboard.client.tools; import com.fasterxml.jackson.databind.JsonNode; -import lombok.RequiredArgsConstructor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -24,43 +28,129 @@ import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.support.HttpRequestWrapper; +import org.springframework.util.StringUtils; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.UpdateMessage; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetSearchQuery; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationInfo; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.security.model.SecuritySettings; +import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.io.IOException; +import java.net.URI; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import static org.springframework.util.StringUtils.isEmpty; + /** * @author Andrew Shvayka */ -@RequiredArgsConstructor public class RestClient implements ClientHttpRequestInterceptor { private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; - private final RestTemplate restTemplate = new RestTemplate(); + protected final RestTemplate restTemplate; + protected final String baseURL; private String token; - private final String baseURL; + private String refreshToken; + private final ObjectMapper objectMapper = new ObjectMapper(); + + + protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; + + public RestClient(String baseURL) { + this.restTemplate = new RestTemplate(); + this.baseURL = baseURL; + } + + public RestClient(RestTemplate restTemplate, String baseURL) { + this.restTemplate = restTemplate; + this.baseURL = baseURL; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException { + HttpRequest wrapper = new HttpRequestWrapper(request); + wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); + ClientHttpResponse response = execution.execute(wrapper, bytes); + if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) { + synchronized (this) { + restTemplate.getInterceptors().remove(this); + refreshToken(); + wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); + return execution.execute(wrapper, bytes); + } + } + return response; + } + + public String getToken() { + return token; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void refreshToken() { + Map refreshTokenRequest = new HashMap<>(); + refreshTokenRequest.put("refreshToken", refreshToken); + ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class); + setTokenInfo(tokenInfo.getBody()); + } public void login(String username, String password) { Map loginRequest = new HashMap<>(); loginRequest.put("username", username); loginRequest.put("password", password); ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class); - this.token = tokenInfo.getBody().get("token").asText(); - restTemplate.setInterceptors(Collections.singletonList(this)); + setTokenInfo(tokenInfo.getBody()); + } + + private void setTokenInfo(JsonNode tokenInfo) { + this.token = tokenInfo.get("token").asText(); + this.refreshToken = tokenInfo.get("refreshToken").asText(); + restTemplate.getInterceptors().add(this); } public Optional findDevice(String name) { @@ -149,10 +239,6 @@ public class RestClient implements ClientHttpRequestInterceptor { return saveDeviceCredentials(deviceCredentials); } - public DeviceCredentials saveDeviceCredentials(DeviceCredentials deviceCredentials) { - return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody(); - } - public Device createDevice(Device device) { return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); } @@ -190,7 +276,7 @@ public class RestClient implements ClientHttpRequestInterceptor { } public Asset assignAsset(CustomerId customerId, AssetId assetId) { - return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class, + return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", HttpEntity.EMPTY, Asset.class, customerId.toString(), assetId.toString()).getBody(); } @@ -202,6 +288,29 @@ public class RestClient implements ClientHttpRequestInterceptor { return restTemplate.postForEntity(baseURL + "/api/relation", relation, EntityRelation.class).getBody(); } + public Dashboard createDashboard(Dashboard dashboard) { + return restTemplate.postForEntity(baseURL + "/api/dashboard", dashboard, Dashboard.class).getBody(); + } + + public void deleteDashboard(DashboardId dashboardId) { + restTemplate.delete(baseURL + "/api/dashboard/{dashboardId}", dashboardId); + } + + public List findTenantDashboards() { + try { + ResponseEntity> dashboards = + restTemplate.exchange(baseURL + "/api/tenant/dashboards?limit=100000", HttpMethod.GET, null, new ParameterizedTypeReference>() { + }); + return dashboards.getBody().getData(); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Collections.emptyList(); + } else { + throw exception; + } + } + } + public DeviceCredentials getCredentials(DeviceId id) { return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody(); } @@ -210,14 +319,1702 @@ public class RestClient implements ClientHttpRequestInterceptor { return restTemplate; } - public String getToken() { - return token; + public Optional getAdminSettings(String key) { + try { + ResponseEntity adminSettings = restTemplate.getForEntity(baseURL + "/api/admin/settings/{key}", AdminSettings.class, key); + return Optional.ofNullable(adminSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } } - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException { - HttpRequest wrapper = new HttpRequestWrapper(request); - wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); - return execution.execute(wrapper, bytes); + public AdminSettings saveAdminSettings(AdminSettings adminSettings) { + return restTemplate.postForEntity(baseURL + "/api/admin/settings", adminSettings, AdminSettings.class).getBody(); + } + + public void sendTestMail(AdminSettings adminSettings) { + restTemplate.postForEntity(baseURL + "/api/admin/settings/testMail", adminSettings, AdminSettings.class); + } + + public Optional getSecuritySettings() { + try { + ResponseEntity securitySettings = restTemplate.getForEntity(baseURL + "/api/admin/securitySettings", SecuritySettings.class); + return Optional.ofNullable(securitySettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public SecuritySettings saveSecuritySettings(SecuritySettings securitySettings) { + return restTemplate.postForEntity(baseURL + "/api/admin/securitySettings", securitySettings, SecuritySettings.class).getBody(); + } + + public Optional checkUpdates() { + try { + ResponseEntity updateMsg = restTemplate.getForEntity(baseURL + "/api/admin/updates", UpdateMessage.class); + return Optional.ofNullable(updateMsg.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getAlarmById(String alarmId) { + try { + ResponseEntity alarm = restTemplate.getForEntity(baseURL + "/api/alarm/{alarmId}", Alarm.class, alarmId); + return Optional.ofNullable(alarm.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getAlarmInfoById(String alarmId) { + try { + ResponseEntity alarmInfo = restTemplate.getForEntity(baseURL + "/api/alarm/info/{alarmId}", AlarmInfo.class, alarmId); + return Optional.ofNullable(alarmInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Alarm saveAlarm(Alarm alarm) { + return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody(); + } + + public void deleteAlarm(String alarmId) { + restTemplate.delete(baseURL + "/api/alarm/{alarmId}", alarmId); + } + + public void ackAlarm(String alarmId) { + restTemplate.postForObject(baseURL + "/api/alarm/{alarmId}/ack", new Object(), Object.class, alarmId); + } + + public void clearAlarm(String alarmId) { + restTemplate.postForObject(baseURL + "/api/alarm/{alarmId}/clear", new Object(), Object.class, alarmId); + } + + public TimePageData getAlarms(String entityType, String entityId, String searchStatus, String status, TimePageLink pageLink, Boolean fetchOriginator) { + Map params = new HashMap<>(); + params.put("entityType", entityType); + params.put("entityId", entityId); + params.put("searchStatus", searchStatus); + params.put("status", status); + params.put("fetchOriginator", String.valueOf(fetchOriginator)); + addPageLinkToParam(params, pageLink); + + String urlParams = getUrlParams(pageLink); + return restTemplate.exchange( + baseURL + "/api/alarm/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}&fetchOriginator={fetchOriginator}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + private String getUrlParams(TimePageLink pageLink) { + String urlParams = "limit={limit}&ascOrder={ascOrder}"; + if (pageLink.getStartTime() != null) { + urlParams += "&startTime={startTime}"; + } + if (pageLink.getEndTime() != null) { + urlParams += "&endTime={endTime}"; + } + if (pageLink.getIdOffset() != null) { + urlParams += "&offset={offset}"; + } + return urlParams; + } + + private String getUrlParams(TextPageLink pageLink) { + String urlParams = "limit={limit}"; + if (!isEmpty(pageLink.getTextSearch())) { + urlParams += "&textSearch={textSearch}"; + } + if (!isEmpty(pageLink.getIdOffset())) { + urlParams += "&idOffset={idOffset}"; + } + if (!isEmpty(pageLink.getTextOffset())) { + urlParams += "&textOffset={textOffset}"; + } + return urlParams; + } + + public Optional getHighestAlarmSeverity(String entityType, String entityId, String searchStatus, String status) { + Map params = new HashMap<>(); + params.put("entityType", entityType); + params.put("entityId", entityId); + params.put("searchStatus", searchStatus); + params.put("status", status); + try { + ResponseEntity alarmSeverity = restTemplate.getForEntity(baseURL + "/api/alarm/highestSeverity/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}", AlarmSeverity.class, params); + return Optional.ofNullable(alarmSeverity.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getAssetById(String assetId) { + try { + ResponseEntity asset = restTemplate.getForEntity(baseURL + "/api/asset/{assetId}", Asset.class, assetId); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Asset saveAsset(Asset asset) { + return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody(); + } + + public void deleteAsset(String assetId) { + restTemplate.delete(baseURL + "/api/asset/{assetId}", assetId); + } + + public Optional assignAssetToCustomer(String customerId, + String assetId) { + Map params = new HashMap<>(); + params.put("customerId", customerId); + params.put("assetId", assetId); + + try { + ResponseEntity asset = restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class, params); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignAssetFromCustomer(String assetId) { + try { + ResponseEntity asset = restTemplate.exchange(baseURL + "/api/customer/asset/{assetId}", HttpMethod.DELETE, HttpEntity.EMPTY, Asset.class, assetId); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignAssetToPublicCustomer(String assetId) { + try { + ResponseEntity asset = restTemplate.postForEntity(baseURL + "/api/customer/public/asset/{assetId}", null, Asset.class, assetId); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public TextPageData getTenantAssets(TextPageLink pageLink, String type) { + Map params = new HashMap<>(); + params.put("type", type); + addPageLinkToParam(params, pageLink); + + ResponseEntity> assets = restTemplate.exchange( + baseURL + "/api/tenant/assets?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return assets.getBody(); + } + + public Optional getTenantAsset(String assetName) { + try { + ResponseEntity asset = restTemplate.getForEntity(baseURL + "/api/tenant/assets?assetName={assetName}", Asset.class, assetName); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public TextPageData getCustomerAssets(String customerId, TextPageLink pageLink, String type) { + Map params = new HashMap<>(); + params.put("customerId", customerId); + params.put("type", type); + addPageLinkToParam(params, pageLink); + + ResponseEntity> assets = restTemplate.exchange( + baseURL + "/api/customer/{customerId}/assets?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return assets.getBody(); + } + + public List getAssetsByIds(String[] assetIds) { + return restTemplate.exchange( + baseURL + "/api/assets?assetIds={assetIds}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + String.join(",", assetIds)).getBody(); + } + + public List findByQuery(AssetSearchQuery query) { + return restTemplate.exchange( + URI.create(baseURL + "/api/assets"), + HttpMethod.POST, + new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getAssetTypes() { + return restTemplate.exchange(URI.create( + baseURL + "/api/asset/types"), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public TimePageData getAuditLogsByCustomerId(String customerId, TimePageLink pageLink, String actionTypes) { + Map params = new HashMap<>(); + params.put("customerId", customerId); + params.put("actionTypes", actionTypes); + addPageLinkToParam(params, pageLink); + + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs/customer/{customerId}?actionTypes={actionTypes}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return auditLog.getBody(); + } + + public TimePageData getAuditLogsByUserId(String userId, TimePageLink pageLink, String actionTypes) { + Map params = new HashMap<>(); + params.put("userId", userId); + params.put("actionTypes", actionTypes); + addPageLinkToParam(params, pageLink); + + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs/user/{userId}?actionTypes={actionTypes}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return auditLog.getBody(); + } + + public TimePageData getAuditLogsByEntityId(String entityType, String entityId, String actionTypes, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("entityType", entityType); + params.put("entityId", entityId); + params.put("actionTypes", actionTypes); + addPageLinkToParam(params, pageLink); + + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs/entity/{entityType}/{entityId}?actionTypes={actionTypes}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return auditLog.getBody(); + } + + public TimePageData getAuditLogs(TimePageLink pageLink, String actionTypes) { + Map params = new HashMap<>(); + params.put("actionTypes", actionTypes); + addPageLinkToParam(params, pageLink); + + ResponseEntity> auditLog = restTemplate.exchange( + baseURL + "/api/audit/logs?actionTypes={actionTypes}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return auditLog.getBody(); + } + + public String getActivateToken(String userId) { + String activationLink = getActivationLink(userId); + return StringUtils.delete(activationLink, baseURL + ACTIVATE_TOKEN_REGEX); + } + + public Optional getUser() { + ResponseEntity user = restTemplate.getForEntity(baseURL + "/api/auth/user", User.class); + return Optional.ofNullable(user.getBody()); + } + + public void logout() { + restTemplate.exchange(URI.create(baseURL + "/api/auth/logout"), HttpMethod.POST, HttpEntity.EMPTY, Object.class); + } + + public void changePassword(String currentPassword, String newPassword) { + ObjectNode changePasswordRequest = objectMapper.createObjectNode(); + changePasswordRequest.put("currentPassword", currentPassword); + changePasswordRequest.put("newPassword", newPassword); + restTemplate.exchange(URI.create(baseURL + "/api/auth/changePassword"), HttpMethod.POST, new HttpEntity<>(changePasswordRequest), Object.class); + } + + public Optional getUserPasswordPolicy() { + try { + ResponseEntity userPasswordPolicy = restTemplate.getForEntity(baseURL + "/api/noauth/userPasswordPolicy", UserPasswordPolicy.class); + return Optional.ofNullable(userPasswordPolicy.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public ResponseEntity checkActivateToken(String userId) { + String activateToken = getActivateToken(userId); + return restTemplate.getForEntity(baseURL + "/api/noauth/activate?activateToken={activateToken}", String.class, activateToken); + } + + public void requestResetPasswordByEmail(String email) { + ObjectNode resetPasswordByEmailRequest = objectMapper.createObjectNode(); + resetPasswordByEmailRequest.put("email", email); + restTemplate.exchange(URI.create(baseURL + "/api/noauth/resetPasswordByEmail"), HttpMethod.POST, new HttpEntity<>(resetPasswordByEmailRequest), Object.class); + } + + public Optional activateUser(String userId, String password) { + ObjectNode activateRequest = objectMapper.createObjectNode(); + activateRequest.put("activateToken", getActivateToken(userId)); + activateRequest.put("password", password); + try { + ResponseEntity jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate", activateRequest, JsonNode.class); + return Optional.ofNullable(jsonNode.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getComponentDescriptorByClazz(String componentDescriptorClazz) { + try { + ResponseEntity componentDescriptor = restTemplate.getForEntity(baseURL + "/api/component/{componentDescriptorClazz}", ComponentDescriptor.class, componentDescriptorClazz); + return Optional.ofNullable(componentDescriptor.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getComponentDescriptorsByType(String componentType) { + return restTemplate.exchange( + baseURL + "/api/components?componentType={componentType}", + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + componentType).getBody(); + } + + public List getComponentDescriptorsByTypes(String[] componentTypes) { + return restTemplate.exchange( + baseURL + "/api/components?componentTypes={componentTypes}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + String.join(",", componentTypes)).getBody(); + } + + public Optional getCustomerById(String customerId) { + try { + ResponseEntity customer = restTemplate.getForEntity(baseURL + "/api/customer/{customerId}", Customer.class, customerId); + return Optional.ofNullable(customer.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getShortCustomerInfoById(String customerId) { + try { + ResponseEntity customerInfo = restTemplate.getForEntity(baseURL + "/api/customer/{customerId}/shortInfo", JsonNode.class, customerId); + return Optional.ofNullable(customerInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public String getCustomerTitleById(String customerId) { + return restTemplate.getForObject(baseURL + "/api/customer/{customerId}/title", String.class, customerId); + } + + public Customer saveCustomer(Customer customer) { + return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody(); + } + + public void deleteCustomer(String customerId) { + restTemplate.delete(baseURL + "/api/customer/{customerId}", customerId); + } + + public TextPageData getCustomers(TextPageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + + ResponseEntity> customer = restTemplate.exchange( + baseURL + "/api/customers?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return customer.getBody(); + } + + public Optional getTenantCustomer(String customerTitle) { + try { + ResponseEntity customer = restTemplate.getForEntity(baseURL + "/api/tenant/customers?customerTitle={customerTitle}", Customer.class, customerTitle); + return Optional.ofNullable(customer.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Long getServerTime() { + return restTemplate.getForObject(baseURL + "/api/dashboard/serverTime", Long.class); + } + + public Long getMaxDatapointsLimit() { + return restTemplate.getForObject(baseURL + "/api/dashboard/maxDatapointsLimit", Long.class); + } + + public Optional getDashboardInfoById(String dashboardId) { + try { + ResponseEntity dashboardInfo = restTemplate.getForEntity(baseURL + "/api/dashboard/info/{dashboardId}", DashboardInfo.class, dashboardId); + return Optional.ofNullable(dashboardInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getDashboardById(String dashboardId) { + try { + ResponseEntity dashboard = restTemplate.getForEntity(baseURL + "/api/dashboard/{dashboardId}", Dashboard.class, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Dashboard saveDashboard(Dashboard dashboard) { + return restTemplate.postForEntity(baseURL + "/api/dashboard", dashboard, Dashboard.class).getBody(); + } + + public void deleteDashboard(String dashboardId) { + restTemplate.delete(baseURL + "/api/dashboard/{dashboardId}", dashboardId); + } + + public Optional assignDashboardToCustomer(String customerId, String dashboardId) { + try { + ResponseEntity dashboard = restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/dashboard/{dashboardId}", null, Dashboard.class, customerId, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignDashboardFromCustomer(String customerId, String dashboardId) { + try { + ResponseEntity dashboard = restTemplate.exchange(baseURL + "/api/customer/{customerId}/dashboard/{dashboardId}", HttpMethod.DELETE, HttpEntity.EMPTY, Dashboard.class, customerId, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional updateDashboardCustomers(String dashboardId, String[] customerIds) { + try { + ResponseEntity dashboard = restTemplate.postForEntity(baseURL + "/api/dashboard/{dashboardId}/customers", customerIds, Dashboard.class, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional addDashboardCustomers(String dashboardId, String[] customerIds) { + try { + ResponseEntity dashboard = restTemplate.postForEntity(baseURL + "/api/dashboard/{dashboardId}/customers/add", customerIds, Dashboard.class, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional removeDashboardCustomers(String dashboardId, String[] customerIds) { + try { + ResponseEntity dashboard = restTemplate.postForEntity(baseURL + "/api/dashboard/{dashboardId}/customers/remove", customerIds, Dashboard.class, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignDashboardToPublicCustomer(String dashboardId) { + try { + ResponseEntity dashboard = restTemplate.postForEntity(baseURL + "/api/customer/public/dashboard/{dashboardId}", null, Dashboard.class, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignDashboardFromPublicCustomer(String dashboardId) { + try { + ResponseEntity dashboard = restTemplate.exchange(baseURL + "/api/customer/public/dashboard/{dashboardId}", HttpMethod.DELETE, HttpEntity.EMPTY, Dashboard.class, dashboardId); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public TextPageData getTenantDashboards(String tenantId, TextPageLink pageLink) { + Map params = new HashMap<>(); + params.put("tenantId", tenantId); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/{tenantId}/dashboards?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params + ).getBody(); + } + + public TextPageData getTenantDashboards(TextPageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/dashboards?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params + ).getBody(); + } + + public TimePageData getCustomerDashboards(String customerId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/dashboards?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params + ).getBody(); + } + + public Optional getDeviceById(String deviceId) { + try { + ResponseEntity device = restTemplate.getForEntity(baseURL + "/api/device/{deviceId}", Device.class, deviceId); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Device saveDevice(Device device) { + return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); + } + + public void deleteDevice(String deviceId) { + restTemplate.delete(baseURL + "/api/device/{deviceId}", deviceId); + } + + public Optional assignDeviceToCustomer(String customerId, String deviceId) { + try { + ResponseEntity device = restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class, customerId, deviceId); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignDeviceFromCustomer(String deviceId) { + try { + ResponseEntity device = restTemplate.exchange(baseURL + "/api/customer/device/{deviceId}", HttpMethod.DELETE, HttpEntity.EMPTY, Device.class, deviceId); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignDeviceToPublicCustomer(String deviceId) { + try { + ResponseEntity device = restTemplate.postForEntity(baseURL + "/api/customer/public/device/{deviceId}", null, Device.class, deviceId); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getDeviceCredentialsByDeviceId(String deviceId) { + try { + ResponseEntity deviceCredentials = restTemplate.getForEntity(baseURL + "/api/device/{deviceId}/credentials", DeviceCredentials.class, deviceId); + return Optional.ofNullable(deviceCredentials.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public DeviceCredentials saveDeviceCredentials(DeviceCredentials deviceCredentials) { + return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody(); + } + + public TextPageData getTenantDevices(String type, TextPageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", type); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/devices?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params) + .getBody(); + } + + public Optional getTenantDevice(String deviceName) { + try { + ResponseEntity device = restTemplate.getForEntity(baseURL + "/api/tenant/devices?deviceName={deviceName}", Device.class, deviceName); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public TextPageData getCustomerDevices(String customerId, String type, TextPageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId); + params.put("type", type); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/devices?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params) + .getBody(); + } + + public List getDevicesByIds(String[] deviceIds) { + return restTemplate.exchange(baseURL + "/api/devices?deviceIds={deviceIds}", + HttpMethod.GET, + HttpEntity.EMPTY, new ParameterizedTypeReference>() { + }, + String.join(",", deviceIds)).getBody(); + } + + public List findByQuery(DeviceSearchQuery query) { + return restTemplate.exchange( + baseURL + "/api/devices", + HttpMethod.POST, + new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getDeviceTypes() { + return restTemplate.exchange( + baseURL + "/api/devices", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public DeferredResult claimDevice(String deviceName, ClaimRequest claimRequest) { + return restTemplate.exchange( + baseURL + "/api/customer/device/{deviceName}/claim", + HttpMethod.POST, + new HttpEntity<>(claimRequest), + new ParameterizedTypeReference>() { + }, + deviceName).getBody(); + } + + public DeferredResult reClaimDevice(String deviceName) { + return restTemplate.exchange( + baseURL + "/api/customer/device/{deviceName}/claim", + HttpMethod.DELETE, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + deviceName).getBody(); + } + + public void saveRelation(EntityRelation relation) { + restTemplate.postForEntity(baseURL + "/api/relation", relation, Object.class); + } + + public void deleteRelation(String fromId, String fromType, String relationType, String relationTypeGroup, String toId, String toType) { + Map params = new HashMap<>(); + params.put("fromId", fromId); + params.put("fromType", fromType); + params.put("relationType", relationType); + params.put("relationTypeGroup", relationTypeGroup); + params.put("toId", toId); + params.put("toType", toType); + restTemplate.delete(baseURL + "/api/relation?fromId={fromId}&fromType={fromType}&relationType={relationType}&relationTypeGroup={relationTypeGroup}&toId={toId}&toType={toType}", params); + } + + public void deleteRelations(String entityId, String entityType) { + restTemplate.delete(baseURL + "/api/relations?entityId={entityId}&entityType={entityType}", entityId, entityType); + } + + public Optional getRelation(String fromId, String fromType, String relationType, String relationTypeGroup, String toId, String toType) { + Map params = new HashMap<>(); + params.put("fromId", fromId); + params.put("fromType", fromType); + params.put("relationType", relationType); + params.put("relationTypeGroup", relationTypeGroup); + params.put("toId", toId); + params.put("toType", toType); + + try { + ResponseEntity entityRelation = restTemplate.getForEntity( + baseURL + "/api/relation?fromId={fromId}&fromType={fromType}&relationType={relationType}&relationTypeGroup={relationTypeGroup}&toId={toId}&toType={toType}", + EntityRelation.class, + params); + return Optional.ofNullable(entityRelation.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List findByFrom(String fromId, String fromType, String relationTypeGroup) { + Map params = new HashMap<>(); + params.put("fromId", fromId); + params.put("fromType", fromType); + params.put("relationTypeGroup", relationTypeGroup); + + return restTemplate.exchange( + baseURL + "/api/relations?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public List findInfoByFrom(String fromId, String fromType, String relationTypeGroup) { + Map params = new HashMap<>(); + params.put("fromId", fromId); + params.put("fromType", fromType); + params.put("relationTypeGroup", relationTypeGroup); + + return restTemplate.exchange( + baseURL + "/api/relations/info?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public List findByFrom(String fromId, String fromType, String relationType, String relationTypeGroup) { + Map params = new HashMap<>(); + params.put("fromId", fromId); + params.put("fromType", fromType); + params.put("relationType", relationType); + params.put("relationTypeGroup", relationTypeGroup); + + return restTemplate.exchange( + baseURL + "/api/relations?fromId={fromId}&fromType={fromType}&relationType={relationType}&relationTypeGroup={relationTypeGroup}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public List findByTo(String toId, String toType, String relationTypeGroup) { + Map params = new HashMap<>(); + params.put("toId", toId); + params.put("toType", toType); + params.put("relationTypeGroup", relationTypeGroup); + + return restTemplate.exchange( + baseURL + "/api/relations?toId={toId}&toType={toType}&relationTypeGroup={relationTypeGroup}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public List findInfoByTo(String toId, String toType, String relationTypeGroup) { + Map params = new HashMap<>(); + params.put("toId", toId); + params.put("toType", toType); + params.put("relationTypeGroup", relationTypeGroup); + + return restTemplate.exchange( + baseURL + "/api/relations?toId={toId}&toType={toType}&relationTypeGroup={relationTypeGroup}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public List findByTo(String toId, String toType, String relationType, String relationTypeGroup) { + Map params = new HashMap<>(); + params.put("toId", toId); + params.put("toType", toType); + params.put("relationType", relationType); + params.put("relationTypeGroup", relationTypeGroup); + + return restTemplate.exchange( + baseURL + "/api/relations?toId={toId}&toType={toType}&relationType={relationType}&relationTypeGroup={relationTypeGroup}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public List findByQuery(EntityRelationsQuery query) { + return restTemplate.exchange( + baseURL + "/api/relations", + HttpMethod.POST, + new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List findInfoByQuery(EntityRelationsQuery query) { + return restTemplate.exchange( + baseURL + "/api/relations", + HttpMethod.POST, + new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional getEntityViewById(String entityViewId) { + try { + ResponseEntity entityView = restTemplate.getForEntity(baseURL + "/api/entityView/{entityViewId}", EntityView.class, entityViewId); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public EntityView saveEntityView(EntityView entityView) { + return restTemplate.postForEntity(baseURL + "/api/entityView", entityView, EntityView.class).getBody(); + } + + public void deleteEntityView(String entityViewId) { + restTemplate.delete(baseURL + "/api/entityView/{entityViewId}", entityViewId); + } + + public Optional getTenantEntityView(String entityViewName) { + try { + ResponseEntity entityView = restTemplate.getForEntity(baseURL + "/api/tenant/entityViews?entityViewName={entityViewName}", EntityView.class, entityViewName); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignEntityViewToCustomer(String customerId, String entityViewId) { + try { + ResponseEntity entityView = restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/entityView/{entityViewId}", null, EntityView.class, customerId, entityViewId); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignEntityViewFromCustomer(String entityViewId) { + try { + ResponseEntity entityView = restTemplate.exchange( + baseURL + "/api/customer/entityView/{entityViewId}", + HttpMethod.DELETE, + HttpEntity.EMPTY, + EntityView.class, entityViewId); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public TextPageData getCustomerEntityViews(String customerId, String type, TextPageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId); + params.put("type", type); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/entityViews?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public TextPageData getTenantEntityViews(String type, TextPageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", type); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/entityViews?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public List findByQuery(EntityViewSearchQuery query) { + return restTemplate.exchange(baseURL + "/api/entityViews", HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getEntityViewTypes() { + return restTemplate.exchange(baseURL + "/api/entityView/types", HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional assignEntityViewToPublicCustomer(String entityViewId) { + try { + ResponseEntity entityView = restTemplate.postForEntity(baseURL + "/api/customer/public/entityView/{entityViewId}", null, EntityView.class, entityViewId); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public TimePageData getEvents(String entityType, String entityId, String eventType, String tenantId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("entityType", entityType); + params.put("entityId", entityId); + params.put("eventType", eventType); + params.put("tenantId", tenantId); + addPageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + "/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public TimePageData getEvents(String entityType, String entityId, String tenantId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("entityType", entityType); + params.put("entityId", entityId); + params.put("tenantId", tenantId); + addPageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + "/api/events/{entityType}/{entityId}?tenantId={tenantId}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public DeferredResult handleOneWayDeviceRPCRequest(String deviceId, String requestBody) { + return restTemplate.exchange( + baseURL + "/api/plugins/rpc/oneway/{deviceId}", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference>() { + }, + deviceId).getBody(); + } + + public DeferredResult handleTwoWayDeviceRPCRequest(String deviceId, String requestBody) { + return restTemplate.exchange( + baseURL + "/api/plugins/rpc/twoway/{deviceId}", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference>() { + }, + deviceId).getBody(); + } + + public Optional getRuleChainById(String ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.getForEntity(baseURL + "/api/ruleChain/{ruleChainId}", RuleChain.class, ruleChainId); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getRuleChainMetaData(String ruleChainId) { + try { + ResponseEntity ruleChainMetaData = restTemplate.getForEntity(baseURL + "/api/ruleChain/{ruleChainId}/metadata", RuleChainMetaData.class, ruleChainId); + return Optional.ofNullable(ruleChainMetaData.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public RuleChain saveRuleChain(RuleChain ruleChain) { + return restTemplate.postForEntity(baseURL + "/api/ruleChain", ruleChain, RuleChain.class).getBody(); + } + + public Optional setRootRuleChain(String ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/root", null, RuleChain.class, ruleChainId); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMetaData) { + return restTemplate.postForEntity(baseURL + "/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class).getBody(); + } + + public TextPageData getRuleChains(TextPageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/ruleChains" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + } + ).getBody(); + } + + public void deleteRuleChain(String ruleChainId) { + restTemplate.delete(baseURL + "/api/ruleChain/{ruleChainId}", ruleChainId); + } + + public Optional getLatestRuleNodeDebugInput(String ruleNodeId) { + try { + ResponseEntity jsonNode = restTemplate.getForEntity(baseURL + "/api/ruleNode/{ruleNodeId}/debugIn", JsonNode.class, ruleNodeId); + return Optional.ofNullable(jsonNode.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional testScript(JsonNode inputParams) { + try { + ResponseEntity jsonNode = restTemplate.postForEntity(baseURL + "/api/ruleChain/testScript", inputParams, JsonNode.class); + return Optional.ofNullable(jsonNode.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public DeferredResult getAttributeKeys(String entityType, String entityId) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/keys/attributes", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + entityType, + entityId).getBody(); + } + + public DeferredResult getAttributeKeysByScope(String entityType, String entityId, String scope) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/keys/attributes/{scope}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + scope).getBody(); + } + + public DeferredResult getAttributesResponseEntity(String entityType, String entityId, String keys) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/values/attributes?keys={keys}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + keys).getBody(); + } + + public DeferredResult getAttributesByScope(String entityType, String entityId, String scope, String keys) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}?keys={keys}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + scope, + keys).getBody(); + } + + public DeferredResult getTimeseriesKeys(String entityType, String entityId) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/keys/timeseries", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + entityType, + entityId).getBody(); + } + + public DeferredResult getLatestTimeseries(String entityType, String entityId, String keys) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + keys).getBody(); + } + + + public DeferredResult getTimeseries(String entityType, String entityId, String keys, Long startTs, Long endTs, Long interval, Integer limit, String agg) { + Map params = new HashMap<>(); + params.put("entityType", entityType); + params.put("entityId", entityId); + params.put("keys", keys); + params.put("startTs", startTs.toString()); + params.put("endTs", endTs.toString()); + params.put("interval", interval == null ? "0" : interval.toString()); + params.put("limit", limit == null ? "100" : limit.toString()); + params.put("agg", agg == null ? "NONE" : agg); + + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&startTs={startTs}&endTs={endTs}&interval={interval}&limit={limit}&agg={agg}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public DeferredResult saveDeviceAttributes(String deviceId, String scope, JsonNode request) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{deviceId}/{scope}", + HttpMethod.POST, + new HttpEntity<>(request), + new ParameterizedTypeReference>() { + }, + deviceId, + scope).getBody(); + } + + public DeferredResult saveEntityAttributesV1(String entityType, String entityId, String scope, JsonNode request) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/{scope}", + HttpMethod.POST, + new HttpEntity<>(request), + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + scope).getBody(); + } + + public DeferredResult saveEntityAttributesV2(String entityType, String entityId, String scope, JsonNode request) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", + HttpMethod.POST, + new HttpEntity<>(request), + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + scope).getBody(); + } + + public DeferredResult saveEntityTelemetry(String entityType, String entityId, String scope, String requestBody) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/{scope}", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + scope).getBody(); + } + + public DeferredResult saveEntityTelemetryWithTTL(String entityType, String entityId, String scope, Long ttl, String requestBody) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/{scope}/{ttl}", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + scope, + ttl).getBody(); + } + + public DeferredResult deleteEntityTimeseries(String entityType, + String entityId, + String keys, + boolean deleteAllDataForKeys, + Long startTs, + Long endTs, + boolean rewriteLatestIfDeleted) { + Map params = new HashMap<>(); + params.put("entityType", entityType); + params.put("entityId", entityId); + params.put("keys", keys); + params.put("deleteAllDataForKeys", String.valueOf(deleteAllDataForKeys)); + params.put("startTs", startTs.toString()); + params.put("endTs", endTs.toString()); + params.put("rewriteLatestIfDeleted", String.valueOf(rewriteLatestIfDeleted)); + + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete?keys={keys}&deleteAllDataForKeys={deleteAllDataForKeys}&startTs={startTs}&endTs={endTs}&rewriteLatestIfDeleted={rewriteLatestIfDeleted}", + HttpMethod.DELETE, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public DeferredResult deleteEntityAttributes(String deviceId, String scope, String keys) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{deviceId}/{scope}?keys={keys}", + HttpMethod.DELETE, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + deviceId, + scope, + keys).getBody(); + } + + public DeferredResult deleteEntityAttributes(String entityType, String entityId, String scope, String keys) { + return restTemplate.exchange( + baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/{scope}?keys={keys}", + HttpMethod.DELETE, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + entityType, + entityId, + scope, + keys).getBody(); + } + + public Optional getTenantById(String tenantId) { + try { + ResponseEntity tenant = restTemplate.getForEntity(baseURL + "/api/tenant/{tenantId}", Tenant.class, tenantId); + return Optional.ofNullable(tenant.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Tenant saveTenant(Tenant tenant) { + return restTemplate.postForEntity(baseURL + "/api/tenant", tenant, Tenant.class).getBody(); + } + + public void deleteTenant(String tenantId) { + restTemplate.delete(baseURL + "/api/tenant/{tenantId}", tenantId); + } + + public TextPageData getTenants(TextPageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenants?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public Optional getUserById(String userId) { + try { + ResponseEntity user = restTemplate.getForEntity(baseURL + "/api/user/{userId}", User.class, userId); + return Optional.ofNullable(user.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Boolean isUserTokenAccessEnabled() { + return restTemplate.getForEntity(baseURL + "/api/user/tokenAccessEnabled", Boolean.class).getBody(); + } + + public Optional getUserToken(String userId) { + try { + ResponseEntity userToken = restTemplate.getForEntity(baseURL + "/api/user/{userId}/token", JsonNode.class, userId); + return Optional.ofNullable(userToken.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public User saveUser(User user, boolean sendActivationMail) { + return restTemplate.postForEntity(baseURL + "/api/user?sendActivationMail={sendActivationMail}", user, User.class, sendActivationMail).getBody(); + } + + public void sendActivationEmail(String email) { + restTemplate.postForEntity(baseURL + "/api/user/sendActivationMail?email={email}", null, Object.class, email); + } + + public String getActivationLink(String userId) { + return restTemplate.getForEntity(baseURL + "/api/user/{userId}/activationLink", String.class, userId).getBody(); + } + + public void deleteUser(String userId) { + restTemplate.delete(baseURL + "/api/user/{userId}", userId); + } + + public TextPageData getTenantAdmins(String tenantId, TextPageLink pageLink) { + Map params = new HashMap<>(); + params.put("tenantId", tenantId); + addPageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + "/api/tenant/{tenantId}/users?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public TextPageData getCustomerUsers(String customerId, TextPageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId); + addPageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/users?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public void setUserCredentialsEnabled(String userId, boolean userCredentialsEnabled) { + restTemplate.postForEntity( + baseURL + "/api/user/{userId}/userCredentialsEnabled?serCredentialsEnabled={serCredentialsEnabled}", + null, + Object.class, + userId, + userCredentialsEnabled); + } + + public Optional getWidgetsBundleById(String widgetsBundleId) { + try { + ResponseEntity widgetsBundle = + restTemplate.getForEntity(baseURL + "/api/widgetsBundle/{widgetsBundleId}", WidgetsBundle.class, widgetsBundleId); + return Optional.ofNullable(widgetsBundle.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public WidgetsBundle saveWidgetsBundle(WidgetsBundle widgetsBundle) { + return restTemplate.postForEntity(baseURL + "/api/widgetsBundle", widgetsBundle, WidgetsBundle.class).getBody(); + } + + public void deleteWidgetsBundle(String widgetsBundleId) { + restTemplate.delete(baseURL + "/api/widgetsBundle/{widgetsBundleId}", widgetsBundleId); + } + + public TextPageData getWidgetsBundles(TextPageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/widgetsBundles?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getWidgetsBundles() { + return restTemplate.exchange( + baseURL + "/api/widgetsBundles", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional getWidgetTypeById(String widgetTypeId) { + try { + ResponseEntity widgetType = + restTemplate.getForEntity(baseURL + "/api/widgetType/{widgetTypeId}", WidgetType.class, widgetTypeId); + return Optional.ofNullable(widgetType.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public WidgetType saveWidgetType(WidgetType widgetType) { + return restTemplate.postForEntity(baseURL + "/api/widgetType", widgetType, WidgetType.class).getBody(); + } + + public void deleteWidgetType(String widgetTypeId) { + restTemplate.delete(baseURL + "/api/widgetType/{widgetTypeId}", widgetTypeId); + } + + public List getBundleWidgetTypes(boolean isSystem, String bundleAlias) { + return restTemplate.exchange( + baseURL + "/api/widgetTypes?isSystem={isSystem}&bundleAlias={bundleAlias}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + isSystem, + bundleAlias).getBody(); + } + + public Optional getWidgetType(boolean isSystem, String bundleAlias, String alias) { + try { + ResponseEntity widgetType = + restTemplate.getForEntity( + baseURL + "/api/widgetType?isSystem={isSystem}&bundleAlias={bundleAlias}&alias={alias}", + WidgetType.class, + isSystem, + bundleAlias, + alias); + return Optional.ofNullable(widgetType.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + private void addPageLinkToParam(Map params, TimePageLink pageLink) { + params.put("limit", String.valueOf(pageLink.getLimit())); + if (pageLink.getStartTime() != null) { + params.put("startTime", String.valueOf(pageLink.getStartTime())); + } + if (pageLink.getEndTime() != null) { + params.put("endTime", String.valueOf(pageLink.getEndTime())); + } + params.put("ascOrder", String.valueOf(pageLink.isAscOrder())); + if (pageLink.getIdOffset() != null) { + params.put("offset", pageLink.getIdOffset().toString()); + } + } + + private void addPageLinkToParam(Map params, TextPageLink pageLink) { + params.put("limit", String.valueOf(pageLink.getLimit())); + if (pageLink.getTextSearch() != null) { + params.put("textSearch", pageLink.getTextSearch()); + } + + if (pageLink.getIdOffset() != null) { + params.put("idOffset", pageLink.getIdOffset().toString()); + + } + if (pageLink.getTextOffset() != null) { + params.put("textOffset", pageLink.getTextOffset()); + } } -} \ No newline at end of file +} diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/MigratorTool.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/MigratorTool.java new file mode 100644 index 0000000000..fdc1917ca8 --- /dev/null +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/MigratorTool.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.client.tools.migrator; + +import org.apache.commons.cli.BasicParser; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.io.File; + +public class MigratorTool { + + public static void main(String[] args) { + CommandLine cmd = parseArgs(args); + + + try { + File latestSource = new File(cmd.getOptionValue("latestTelemetryFrom")); + File latestSaveDir = new File(cmd.getOptionValue("latestTelemetryOut")); + File tsSource = new File(cmd.getOptionValue("telemetryFrom")); + File tsSaveDir = new File(cmd.getOptionValue("telemetryOut")); + File partitionsSaveDir = new File(cmd.getOptionValue("partitionsOut")); + boolean castEnable = Boolean.parseBoolean(cmd.getOptionValue("castEnable")); + + PgCaLatestMigrator.migrateLatest(latestSource, latestSaveDir, castEnable); + PostgresToCassandraTelemetryMigrator.migrateTs(tsSource, tsSaveDir, partitionsSaveDir, castEnable); + + } catch (Throwable th) { + th.printStackTrace(); + throw new IllegalStateException("failed", th); + } + + } + + private static CommandLine parseArgs(String[] args) { + Options options = new Options(); + + Option latestTsOpt = new Option("latestFrom", "latestTelemetryFrom", true, "latest telemetry source file path"); + latestTsOpt.setRequired(true); + options.addOption(latestTsOpt); + + Option latestTsOutOpt = new Option("latestOut", "latestTelemetryOut", true, "latest telemetry save dir"); + latestTsOutOpt.setRequired(true); + options.addOption(latestTsOutOpt); + + Option tsOpt = new Option("tsFrom", "telemetryFrom", true, "telemetry source file path"); + tsOpt.setRequired(true); + options.addOption(tsOpt); + + Option tsOutOpt = new Option("tsOut", "telemetryOut", true, "sstable save dir"); + tsOutOpt.setRequired(true); + options.addOption(tsOutOpt); + + Option partitionOutOpt = new Option("partitionsOut", "partitionsOut", true, "partitions save dir"); + partitionOutOpt.setRequired(true); + options.addOption(partitionOutOpt); + + Option castOpt = new Option("castEnable", "castEnable", true, "cast String to Double if possible"); + castOpt.setRequired(true); + options.addOption(castOpt); + + HelpFormatter formatter = new HelpFormatter(); + CommandLineParser parser = new BasicParser(); + + try { + return parser.parse(options, args); + } catch (ParseException e) { + System.out.println(e.getMessage()); + formatter.printHelp("utility-name", options); + + System.exit(1); + } + return null; + } + +} diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaLatestMigrator.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaLatestMigrator.java new file mode 100644 index 0000000000..667f5e6d0f --- /dev/null +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaLatestMigrator.java @@ -0,0 +1,179 @@ +/** + * 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.client.tools.migrator; + +import com.google.common.collect.Lists; +import org.apache.cassandra.io.sstable.CQLSSTableWriter; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.LineIterator; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class PgCaLatestMigrator { + + private static final long LOG_BATCH = 1000000; + private static final long rowPerFile = 1000000; + + + private static long linesProcessed = 0; + private static long linesMigrated = 0; + private static long castErrors = 0; + private static long castedOk = 0; + + private static long currentWriterCount = 1; + private static CQLSSTableWriter currentTsWriter = null; + + public static void migrateLatest(File sourceFile, File outDir, boolean castStringsIfPossible) throws IOException { + long startTs = System.currentTimeMillis(); + long stepLineTs = System.currentTimeMillis(); + long stepOkLineTs = System.currentTimeMillis(); + LineIterator iterator = FileUtils.lineIterator(sourceFile); + currentTsWriter = WriterBuilder.getTsWriter(outDir); + + boolean isBlockStarted = false; + boolean isBlockFinished = false; + + String line; + while (iterator.hasNext()) { + if (linesProcessed++ % LOG_BATCH == 0) { + System.out.println(new Date() + " linesProcessed = " + linesProcessed + " in " + (System.currentTimeMillis() - stepLineTs) + " castOk " + castedOk + " castErr " + castErrors); + stepLineTs = System.currentTimeMillis(); + } + + line = iterator.nextLine(); + + if (isBlockFinished) { + break; + } + + if (!isBlockStarted) { + if (isBlockStarted(line)) { + System.out.println(); + System.out.println(); + System.out.println(line); + System.out.println(); + System.out.println(); + isBlockStarted = true; + } + continue; + } + + if (isBlockFinished(line)) { + isBlockFinished = true; + } else { + try { + List raw = Arrays.stream(line.trim().split("\t")) + .map(String::trim) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + List values = toValues(raw); + + if (currentWriterCount == 0) { + System.out.println(new Date() + " close writer " + new Date()); + currentTsWriter.close(); + currentTsWriter = WriterBuilder.getLatestWriter(outDir); + } + + if (castStringsIfPossible) { + currentTsWriter.addRow(castToNumericIfPossible(values)); + } else { + currentTsWriter.addRow(values); + } + currentWriterCount++; + if (currentWriterCount >= rowPerFile) { + currentWriterCount = 0; + } + + if (linesMigrated++ % LOG_BATCH == 0) { + System.out.println(new Date() + " migrated = " + linesMigrated + " in " + (System.currentTimeMillis() - stepOkLineTs)); + stepOkLineTs = System.currentTimeMillis(); + } + } catch (Exception ex) { + System.out.println(ex.getMessage() + " -> " + line); + } + + } + } + + long endTs = System.currentTimeMillis(); + System.out.println(); + System.out.println(new Date() + " Migrated rows " + linesMigrated + " in " + (endTs - startTs)); + + currentTsWriter.close(); + System.out.println(); + System.out.println("Finished migrate Latest Telemetry"); + } + + + private static List castToNumericIfPossible(List values) { + try { + if (values.get(6) != null && NumberUtils.isNumber(values.get(6).toString())) { + Double casted = NumberUtils.createDouble(values.get(6).toString()); + List numeric = Lists.newArrayList(); + numeric.addAll(values); + numeric.set(6, null); + numeric.set(8, casted); + castedOk++; + return numeric; + } + } catch (Throwable th) { + castErrors++; + } + return values; + } + + private static List toValues(List raw) { + //expected Table structure: +// COPY public.ts_kv_latest (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) FROM stdin; + + + List result = new ArrayList<>(); + result.add(raw.get(0)); + result.add(fromString(raw.get(1))); + result.add(raw.get(2)); + + long ts = Long.parseLong(raw.get(3)); + result.add(ts); + + result.add(raw.get(4).equals("\\N") ? null : raw.get(4).equals("t") ? Boolean.TRUE : Boolean.FALSE); + result.add(raw.get(5).equals("\\N") ? null : raw.get(5)); + result.add(raw.get(6).equals("\\N") ? null : Long.parseLong(raw.get(6))); + result.add(raw.get(7).equals("\\N") ? null : Double.parseDouble(raw.get(7))); + return result; + } + + public static UUID fromString(String src) { + return UUID.fromString(src.substring(7, 15) + "-" + src.substring(3, 7) + "-1" + + src.substring(0, 3) + "-" + src.substring(15, 19) + "-" + src.substring(19)); + } + + private static boolean isBlockStarted(String line) { + return line.startsWith("COPY public.ts_kv_latest"); + } + + private static boolean isBlockFinished(String line) { + return StringUtils.isBlank(line) || line.equals("\\."); + } +} diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/PostgresToCassandraTelemetryMigrator.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/PostgresToCassandraTelemetryMigrator.java new file mode 100644 index 0000000000..fd8562ce3a --- /dev/null +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/PostgresToCassandraTelemetryMigrator.java @@ -0,0 +1,222 @@ +/** + * 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.client.tools.migrator; + +import com.google.common.collect.Lists; +import org.apache.cassandra.io.sstable.CQLSSTableWriter; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.LineIterator; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class PostgresToCassandraTelemetryMigrator { + + private static final long LOG_BATCH = 1000000; + private static final long rowPerFile = 1000000; + + + private static long linesProcessed = 0; + private static long linesMigrated = 0; + private static long castErrors = 0; + private static long castedOk = 0; + + private static long currentWriterCount = 1; + private static CQLSSTableWriter currentTsWriter = null; + private static CQLSSTableWriter currentPartitionWriter = null; + + private static Set partitions = new HashSet<>(); + + + public static void migrateTs(File sourceFile, File outTsDir, File outPartitionDir, boolean castStringsIfPossible) throws IOException { + long startTs = System.currentTimeMillis(); + long stepLineTs = System.currentTimeMillis(); + long stepOkLineTs = System.currentTimeMillis(); + LineIterator iterator = FileUtils.lineIterator(sourceFile); + currentTsWriter = WriterBuilder.getTsWriter(outTsDir); + currentPartitionWriter = WriterBuilder.getPartitionWriter(outPartitionDir); + + boolean isBlockStarted = false; + boolean isBlockFinished = false; + + String line; + while (iterator.hasNext()) { + if (linesProcessed++ % LOG_BATCH == 0) { + System.out.println(new Date() + " linesProcessed = " + linesProcessed + " in " + (System.currentTimeMillis() - stepLineTs) + " castOk " + castedOk + " castErr " + castErrors); + stepLineTs = System.currentTimeMillis(); + } + + line = iterator.nextLine(); + + if (isBlockFinished) { + break; + } + + if (!isBlockStarted) { + if (isBlockStarted(line)) { + System.out.println(); + System.out.println(); + System.out.println(line); + System.out.println(); + System.out.println(); + isBlockStarted = true; + } + continue; + } + + if (isBlockFinished(line)) { + isBlockFinished = true; + } else { + try { + List raw = Arrays.stream(line.trim().split("\t")) + .map(String::trim) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + List values = toValues(raw); + + if (currentWriterCount == 0) { + System.out.println(new Date() + " close writer " + new Date()); + currentTsWriter.close(); + currentTsWriter = WriterBuilder.getTsWriter(outTsDir); + } + + if (castStringsIfPossible) { + currentTsWriter.addRow(castToNumericIfPossible(values)); + } else { + currentTsWriter.addRow(values); + } + processPartitions(values); + currentWriterCount++; + if (currentWriterCount >= rowPerFile) { + currentWriterCount = 0; + } + + if (linesMigrated++ % LOG_BATCH == 0) { + System.out.println(new Date() + " migrated = " + linesMigrated + " in " + (System.currentTimeMillis() - stepOkLineTs) + " partitions = " + partitions.size()); + stepOkLineTs = System.currentTimeMillis(); + } + } catch (Exception ex) { + System.out.println(ex.getMessage() + " -> " + line); + } + + } + } + + long endTs = System.currentTimeMillis(); + System.out.println(); + System.out.println(new Date() + " Migrated rows " + linesMigrated + " in " + (endTs - startTs)); + System.out.println("Partitions collected " + partitions.size()); + + startTs = System.currentTimeMillis(); + for (String partition : partitions) { + String[] split = partition.split("\\|"); + List values = Lists.newArrayList(); + values.add(split[0]); + values.add(UUID.fromString(split[1])); + values.add(split[2]); + values.add(Long.parseLong(split[3])); + currentPartitionWriter.addRow(values); + } + currentPartitionWriter.close(); + endTs = System.currentTimeMillis(); + System.out.println(); + System.out.println(); + System.out.println(new Date() + " Migrated partitions " + partitions.size() + " in " + (endTs - startTs)); + + + currentTsWriter.close(); + System.out.println(); + System.out.println("Finished migrate Telemetry"); + } + + private static List castToNumericIfPossible(List values) { + try { + if (values.get(6) != null && NumberUtils.isNumber(values.get(6).toString())) { + Double casted = NumberUtils.createDouble(values.get(6).toString()); + List numeric = Lists.newArrayList(); + numeric.addAll(values); + numeric.set(6, null); + numeric.set(8, casted); + castedOk++; + return numeric; + } + } catch (Throwable th) { + castErrors++; + } + return values; + } + + private static void processPartitions(List values) { + String key = values.get(0) + "|" + values.get(1) + "|" + values.get(2) + "|" + values.get(3); + partitions.add(key); + } + + private static List toValues(List raw) { + //expected Table structure: +// COPY public.ts_kv (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) FROM stdin; + + + List result = new ArrayList<>(); + result.add(raw.get(0)); + result.add(fromString(raw.get(1))); + result.add(raw.get(2)); + + long ts = Long.parseLong(raw.get(3)); + long partition = toPartitionTs(ts); + result.add(partition); + result.add(ts); + + result.add(raw.get(4).equals("\\N") ? null : raw.get(4).equals("t") ? Boolean.TRUE : Boolean.FALSE); + result.add(raw.get(5).equals("\\N") ? null : raw.get(5)); + result.add(raw.get(6).equals("\\N") ? null : Long.parseLong(raw.get(6))); + result.add(raw.get(7).equals("\\N") ? null : Double.parseDouble(raw.get(7))); + return result; + } + + public static UUID fromString(String src) { + return UUID.fromString(src.substring(7, 15) + "-" + src.substring(3, 7) + "-1" + + src.substring(0, 3) + "-" + src.substring(15, 19) + "-" + src.substring(19)); + } + + private static long toPartitionTs(long ts) { + LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC); + return time.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1).toInstant(ZoneOffset.UTC).toEpochMilli(); +// return TsPartitionDate.MONTHS.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli(); + } + + private static boolean isBlockStarted(String line) { + return line.startsWith("COPY public.ts_kv"); + } + + private static boolean isBlockFinished(String line) { + return StringUtils.isBlank(line) || line.equals("\\."); + } + +} diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/README.md b/tools/src/main/java/org/thingsboard/client/tools/migrator/README.md new file mode 100644 index 0000000000..70c5dafcaf --- /dev/null +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/README.md @@ -0,0 +1,87 @@ +# Description: +This tool used for migrating ThingsBoard into hybrid mode from Postgres. + +Performance of this tool depends on disk type and instance type (mostly on CPU resources). +But in general here are few benchmarks: +1. Creating Dump of the postgres ts_kv table -> 100GB = 90 minutes +2. If postgres table has size 100GB then dump file will be about 30GB +3. Generation SSTables from dump -> 100GB = 3 hours +4. 100GB Dump file will be converted into SSTable with size about 18GB + +# Tool build Instruction: +Switch to `tools` module in Command Line and execute + + mvn clean compile assembly:single + +It will generate single jar file with all required dependencies inside `target dir` -> `tools-2.4.1-SNAPSHOT-jar-with-dependencies.jar`. + + +# Prepare requred files and run Tool: + +#### Dump data from the source Postgres Database +*Do not use compression if possible because Tool can only work with uncompressed file + +1. Dump table `ts_kv` table: + + `pg_dump -h localhost -U postgres -d thingsboard -t ts_kv > ts_kv.dmp` + +2. Dump table `ts_kv_latest` table: + + `pg_dump -h localhost -U postgres -d thingsboard -t ts_kv_latest > ts_kv_latest.dmp` + +3. [Optional] move table dumps to the instance where cassandra will be hosted + +#### Prepare directory structure for SSTables +Tool use 3 different directories for saving SSTables - `ts_kv_cf`, `ts_kv_latest_cf`, `ts_kv_partitions_cf` + +Create 3 empty directories. For example: + + /home/ubunut/migration/ts + /home/ubunut/migration/ts_latest + /home/ubunut/migration/ts_partition + +#### Run tool +*Note: if you run this tool on remote instance - don't forget to execute this command in `screen` to avoid unexpected termination + +``` +java -jar ./tools-2.4.1-SNAPSHOT-jar-with-dependencies.jar + -latestFrom ./source/ts_kv_latest.dmp + -latestOut /home/ubunut/migration/ts_latest + -tsFrom ./source/ts_kv.dmp + -tsOut /home/ubunut/migration/ts + -partitionsOut /home/ubunut/migration/ts_partition + -castEnable false +``` + +Tool execution time depends on DB size, CPU resources and Disk throughput + +## Adding SSTables into Cassandra +* Note that this this part works only for single node Cassandra Cluster. If you have more nodes - it is better to use `sstableloader` tool. + +1. [Optional] install Cassandra on the instance +2. [Optional] Using `cqlsh` create `thingsboard` keyspace and requred tables from this file `schema-ts.cql` +3. Stop Cassandra +4. Copy generated SSTable files into cassandra data dir: + +``` + sudo find /home/ubunut/migration/ts -name '*.*' -exec mv {} /var/lib/cassandra/data/thingsboard/ts_kv_cf-0e9aaf00ee5511e9a5fa7d6f489ffd13/ \; + sudo find /home/ubunut/migration/ts_latest -name '*.*' -exec mv {} /var/lib/cassandra/data/thingsboard/ts_kv_latest_cf-161449d0ee5511e9a5fa7d6f489ffd13/ \; + sudo find /home/ubunut/migration/ts_partition -name '*.*' -exec mv {} /var/lib/cassandra/data/thingsboard/ts_kv_partitions_cf-12e8fa80ee5511e9a5fa7d6f489ffd13/ \; +``` + +5. Start Cassandra service and trigger compaction + +``` + trigger compactions: nodetool compact thingsboard + check compaction status: nodetool compactionstats +``` + +## Switch Thignsboard into Hybrid Mode + +Modify Thingsboard properites file `thingsboard.yml` + + - DATABASE_TS_TYPE = cassandra + - TS_KV_PARTITIONING = MONTHS + +# Final steps +Start Thingsboard and verify migration \ No newline at end of file diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java new file mode 100644 index 0000000000..73be1a7fd1 --- /dev/null +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java @@ -0,0 +1,84 @@ +/** + * 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.client.tools.migrator; + +import org.apache.cassandra.io.sstable.CQLSSTableWriter; + +import java.io.File; + +public class WriterBuilder { + + private static final String tsSchema = "CREATE TABLE thingsboard.ts_kv_cf (\n" + + " entity_type text, // (DEVICE, CUSTOMER, TENANT)\n" + + " entity_id timeuuid,\n" + + " key text,\n" + + " partition bigint,\n" + + " ts bigint,\n" + + " bool_v boolean,\n" + + " str_v text,\n" + + " long_v bigint,\n" + + " dbl_v double,\n" + + " PRIMARY KEY (( entity_type, entity_id, key, partition ), ts)\n" + + ");"; + + private static final String latestSchema = "CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf (\n" + + " entity_type text, // (DEVICE, CUSTOMER, TENANT)\n" + + " entity_id timeuuid,\n" + + " key text,\n" + + " ts bigint,\n" + + " bool_v boolean,\n" + + " str_v text,\n" + + " long_v bigint,\n" + + " dbl_v double,\n" + + " PRIMARY KEY (( entity_type, entity_id ), key)\n" + + ") WITH compaction = { 'class' : 'LeveledCompactionStrategy' };"; + + private static final String partitionSchema = "CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf (\n" + + " entity_type text, // (DEVICE, CUSTOMER, TENANT)\n" + + " entity_id timeuuid,\n" + + " key text,\n" + + " partition bigint,\n" + + " PRIMARY KEY (( entity_type, entity_id, key ), partition)\n" + + ") WITH CLUSTERING ORDER BY ( partition ASC )\n" + + " AND compaction = { 'class' : 'LeveledCompactionStrategy' };"; + + public static CQLSSTableWriter getTsWriter(File dir) { + return CQLSSTableWriter.builder() + .inDirectory(dir) + .forTable(tsSchema) + .using("INSERT INTO thingsboard.ts_kv_cf (entity_type, entity_id, key, partition, ts, bool_v, str_v, long_v, dbl_v) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)") + .build(); + } + + public static CQLSSTableWriter getLatestWriter(File dir) { + return CQLSSTableWriter.builder() + .inDirectory(dir) + .forTable(latestSchema) + .using("INSERT INTO thingsboard.ts_kv_latest_cf (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)") + .build(); + } + + public static CQLSSTableWriter getPartitionWriter(File dir) { + return CQLSSTableWriter.builder() + .inDirectory(dir) + .forTable(partitionSchema) + .using("INSERT INTO thingsboard.ts_kv_partitions_cf (entity_type, entity_id, key, partition) " + + "VALUES (?, ?, ?, ?)") + .build(); + } +} diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 08629c2a5e..482153edcc 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 8722273815..2db03f29aa 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index badb11f6cd..7b34b404d3 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index c830bbc009..7c8393e6f6 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard transport diff --git a/ui/package-lock.json b/ui/package-lock.json index f2f85079de..32dc7ca666 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "2.4.1", + "version": "2.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -39,7 +39,7 @@ "@babel/code-frame": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "integrity": "sha1-BuKrGb21NThVWaq7W6WXKUgoAPg=", "dev": true, "requires": { "@babel/highlight": "^7.0.0" @@ -277,7 +277,7 @@ "@babel/helper-function-name": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "integrity": "sha1-oM6wFoX3M1XUNgwSR/WCv6/I/1M=", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.0.0", @@ -288,7 +288,7 @@ "@babel/helper-get-function-arity": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "integrity": "sha1-g1ctQyDipGVyY3NBE8QoaLZOScM=", "dev": true, "requires": { "@babel/types": "^7.0.0" @@ -669,7 +669,7 @@ "@babel/highlight": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "integrity": "sha1-9xDDjI1Fjm3ZogGvtjf8t4HOmeQ=", "dev": true, "requires": { "chalk": "^2.0.0", @@ -680,7 +680,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -706,7 +706,7 @@ "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -1661,12 +1661,12 @@ "@flowjs/ng-flow": { "version": "2.7.8", "resolved": "https://registry.npmjs.org/@flowjs/ng-flow/-/ng-flow-2.7.8.tgz", - "integrity": "sha512-zO6jNvz41oMOJj9+1N+vLT0ytitbCtuGABJQRzQDOPXyRMmlSXfJ7om5oYOztyUFrr4jDpE4QFPt+r2/RFceCg==" + "integrity": "sha1-HZ+dH4Ks2lNgMowxW6z9YNv9mBk=" }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "integrity": "sha1-UkryQNGjYFJ7cwR17PoTRKpUDd4=", "dev": true, "requires": { "call-me-maybe": "^1.0.1", @@ -1912,7 +1912,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" }, "accepts": { "version": "1.3.7", @@ -2018,7 +2018,7 @@ "angular-carousel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/angular-carousel/-/angular-carousel-1.1.0.tgz", - "integrity": "sha512-UiLMgT7Ueqk4xpliF1gWt4dYKXezdJA1jyZPNsUWkOGO/dwLuKi284h3BgWl4CnaH7kEBw8L2gsBOyqbYaumNQ==" + "integrity": "sha1-PmlA5ovRio85L8Qx2XGSrDSIMdE=" }, "angular-cookies": { "version": "1.5.8", @@ -2039,7 +2039,7 @@ } }, "angular-fullscreen": { - "version": "git://github.com/fabiobiondi/angular-fullscreen.git#119b7fbac911d154fd56ace38ebe3432475e8a20", + "version": "git://github.com/fabiobiondi/angular-fullscreen.git#8217174565761d3566807bc60a73b5ca015b8cb6", "from": "git://github.com/fabiobiondi/angular-fullscreen.git#master" }, "angular-gridster": { @@ -2113,7 +2113,7 @@ "angular-translate": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.1.tgz", - "integrity": "sha512-Mw0kFBqsv5j8ItL9IhRZunIlVmIRW6iFsiTmRs9wGr2QTt8z4rehYlWyHos8qnXc/kyOYJiW50iH50CSNHGB9A==", + "integrity": "sha1-sp7Q0vm6xEB156rTKEFmxZ4VB5E=", "requires": { "angular": ">=1.2.26 <=1.7" } @@ -2121,7 +2121,7 @@ "angular-translate-handler-log": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/angular-translate-handler-log/-/angular-translate-handler-log-2.18.1.tgz", - "integrity": "sha512-TyKzCW4GubNazwCgLpCVXd2212CWdZOckf+aL5+gLuThPhVpOvlg18RSmz8MNPto3kwCcCw3LzShlZ6RX/MQRA==", + "integrity": "sha1-icu1mCeALYb4EVJ1+/iNbYiWsNQ=", "requires": { "angular-translate": "~2.18.1" } @@ -2129,7 +2129,7 @@ "angular-translate-interpolation-messageformat": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/angular-translate-interpolation-messageformat/-/angular-translate-interpolation-messageformat-2.18.1.tgz", - "integrity": "sha512-SlmyxLB/UUy7FWoGx5QJHrhq8fUu/xzCR0h/ngexOtXZopQjs1vm+TrFZ69d4c/LI7C91sfP4mq4ES29o1xCxA==", + "integrity": "sha1-FsUq4MYcJA8PJBZKBSGUPPi6QI4=", "requires": { "angular-translate": "~2.18.1", "messageformat": "~1.0.2" @@ -2138,7 +2138,7 @@ "angular-translate-loader-static-files": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/angular-translate-loader-static-files/-/angular-translate-loader-static-files-2.18.1.tgz", - "integrity": "sha512-5MuyzAROfc493kjLjKlLGLBzXiRmZIFbcWZGutDRxW5SRXSpwrH0u0hh0ENNnUyUQbe2vUspHNPIuZqlq8qIhw==", + "integrity": "sha1-rQw8iDsYsIm9uNsCu9Nm2QP4V8w=", "requires": { "angular-translate": "~2.18.1" } @@ -2146,7 +2146,7 @@ "angular-translate-storage-cookie": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/angular-translate-storage-cookie/-/angular-translate-storage-cookie-2.18.1.tgz", - "integrity": "sha512-wiMaF/0OGN/3ilaYunfsqdLNpfGZEJK0fj4zT8yjD3XPq7Q9kM88xZ4XJiWKgodZShBljGCRzqgQbKMF7d1MLw==", + "integrity": "sha1-j8vaspb6gkkOALQorxp0ahf0QVY=", "requires": { "angular-cookies": ">=1.2.26 <1.8", "angular-translate": "~2.18.1" @@ -2155,7 +2155,7 @@ "angular-translate-storage-local": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/angular-translate-storage-local/-/angular-translate-storage-local-2.18.1.tgz", - "integrity": "sha512-zPxcbIJ8tdWXtWNKLtaswynKid0w5le6WPMwiLWhgKPnyzOp/y5WLBW+JEfnZnkGE24yOGhJ6jVPgRNzelLgzg==", + "integrity": "sha1-lHQP5NgBq3gpopofBeHDkFTIcwM=", "requires": { "angular-translate": "~2.18.1", "angular-translate-storage-cookie": "~2.18.1" @@ -2234,7 +2234,7 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=", "dev": true }, "are-we-there-yet": { @@ -2250,7 +2250,7 @@ "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", "dev": true, "requires": { "sprintf-js": "~1.0.2" @@ -2265,7 +2265,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", "dev": true }, "arr-union": { @@ -2366,7 +2366,7 @@ }, "util": { "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -2426,7 +2426,7 @@ "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "integrity": "sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k=", "dev": true }, "attr-accept": { @@ -2681,7 +2681,7 @@ "base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", "dev": true, "requires": { "cache-base": "^1.0.1", @@ -2705,7 +2705,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -2714,7 +2714,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -2723,7 +2723,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -2740,7 +2740,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", "dev": true } } @@ -2768,7 +2768,7 @@ "big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "integrity": "sha1-pfwpi4G54Nyi5FiCR4S2XFK6WI4=", "dev": true }, "binary-extensions": { @@ -2871,7 +2871,7 @@ "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3091,7 +3091,7 @@ "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", "dev": true, "requires": { "collection-visit": "^1.0.0", @@ -3130,7 +3130,7 @@ "dependencies": { "callsites": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true } @@ -3290,13 +3290,13 @@ "circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "integrity": "sha1-gVyZ6oT2gJUp0vRXkb34JxE1LWY=", "dev": true }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", "dev": true, "requires": { "arr-union": "^3.1.0", @@ -3437,7 +3437,7 @@ "clone-regexp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz", - "integrity": "sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw==", + "integrity": "sha1-BRgFzTMXM3XYIRj8CRhgbaOf1g8=", "dev": true, "requires": { "is-regexp": "^1.0.0", @@ -3609,7 +3609,7 @@ "concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -3672,7 +3672,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", "dev": true }, "convert-source-map": { @@ -3699,7 +3699,7 @@ "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "integrity": "sha1-kilzmMrjSTf8r9bsgTnBgFHwteA=", "dev": true, "requires": { "aproba": "^1.1.1", @@ -3960,7 +3960,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=", "dev": true }, "js-yaml": { @@ -4025,7 +4025,7 @@ "create-react-class": { "version": "15.6.3", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", - "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", + "integrity": "sha1-LXMjf7P5cK5uvgEanmb0bbyoADY=", "requires": { "fbjs": "^0.8.9", "loose-envify": "^1.3.1", @@ -4192,7 +4192,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", "dev": true, "requires": { "ms": "2.0.0" @@ -4317,7 +4317,7 @@ "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", "dev": true, "requires": { "is-descriptor": "^1.0.2", @@ -4327,7 +4327,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -4336,7 +4336,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -4345,7 +4345,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -4362,7 +4362,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", "dev": true } } @@ -4399,7 +4399,7 @@ "delegate": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + "integrity": "sha1-tmtxwxWFIuirV0T3INjKDCr1kWY=" }, "delegates": { "version": "1.0.0", @@ -4464,7 +4464,7 @@ "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "integrity": "sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=", "dev": true, "requires": { "pify": "^3.0.0" @@ -4564,7 +4564,7 @@ "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "integrity": "sha1-PTH1AZGmdJ3RN1p/Ui6CPULlTto=", "dev": true }, "domelementtype": { @@ -4595,7 +4595,7 @@ "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "integrity": "sha1-HxngwuGqDjJ5fEl5nyg3rGr2nFc=", "dev": true, "requires": { "is-obj": "^1.0.0" @@ -4690,7 +4690,7 @@ "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", "dev": true, "requires": { "once": "^1.4.0" @@ -4716,7 +4716,7 @@ "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "integrity": "sha1-RoTXF3mtOa8Xfj8AeZb3xnyFJhg=", "dev": true, "requires": { "prr": "~1.0.1" @@ -5356,7 +5356,7 @@ "esquery": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "integrity": "sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=", "dev": true, "requires": { "estraverse": "^4.0.0" @@ -5365,7 +5365,7 @@ "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", "dev": true, "requires": { "estraverse": "^4.1.0" @@ -5622,7 +5622,7 @@ "external-editor": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "integrity": "sha1-BFURz9jRM/OEZnPRBHwVTiFK09U=", "requires": { "chardet": "^0.4.0", "iconv-lite": "^0.4.17", @@ -6035,7 +6035,7 @@ "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "integrity": "sha1-4y/AMKLM7kSmtTcTCNpUvgs5fSc=", "dev": true }, "fs-write-stream-atomic": { @@ -6097,14 +6097,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6119,20 +6117,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6249,8 +6244,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6262,7 +6256,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6277,7 +6270,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6285,14 +6277,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6311,7 +6301,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6392,8 +6381,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6405,7 +6393,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6528,7 +6515,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6618,7 +6604,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", "dev": true }, "functional-red-black-tree": { @@ -6847,7 +6833,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -6860,7 +6846,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -8004,7 +7990,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=" }, "inline-style-prefixer": { "version": "2.0.5", @@ -8054,7 +8040,7 @@ "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=", "dev": true, "requires": { "loose-envify": "^1.0.0" @@ -8127,7 +8113,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", "dev": true }, "is-callable": { @@ -8160,7 +8146,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", @@ -8171,7 +8157,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", "dev": true } } @@ -8286,7 +8272,7 @@ "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "dev": true, "requires": { "isobject": "^3.0.1" @@ -8340,7 +8326,7 @@ "is-supported-regexp-flag": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.1.tgz", - "integrity": "sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==", + "integrity": "sha1-Ie4WUY0sHdPt0+mg1X5QIHrDZMo=", "dev": true }, "is-symbol": { @@ -8373,7 +8359,7 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", "dev": true }, "is-word-character": { @@ -8528,7 +8514,7 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "integrity": "sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk=", "dev": true }, "json-schema": { @@ -8545,7 +8531,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -8672,6 +8658,11 @@ "resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz", "integrity": "sha1-RGf0n5jRv9VpWb2cZwUgPdJgEnc=" }, + "leaflet.markercluster": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz", + "integrity": "sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw==" + }, "less": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz", @@ -8748,7 +8739,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -8854,7 +8845,7 @@ "lodash.merge": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" + "integrity": "sha1-rcJdnLmbk5HFliTzefu6YNcRHVQ=" }, "lodash.tail": { "version": "4.1.1", @@ -8870,7 +8861,7 @@ "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "integrity": "sha1-V0Dhxdbw39pK2TI7UzIQfva0xAo=", "dev": true, "requires": { "chalk": "^2.0.1" @@ -8879,7 +8870,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -8899,7 +8890,7 @@ "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -9199,7 +9190,7 @@ "messageformat-parser": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-1.1.0.tgz", - "integrity": "sha512-Hwem6G3MsKDLS1FtBRGIs8T50P1Q00r3srS6QJePCFbad9fq0nYxwf3rnU2BreApRGhmpKMV7oZI06Sy1c9TPA==" + "integrity": "sha1-E7oiUKdrvejg/KDbs0dflcWUqQo=" }, "methods": { "version": "1.1.2", @@ -9249,7 +9240,7 @@ "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=", "dev": true }, "mime-db": { @@ -9270,7 +9261,7 @@ "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=" }, "min-document": { "version": "2.19.0", @@ -9308,7 +9299,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { "brace-expansion": "^1.1.7" } @@ -9321,7 +9312,7 @@ "minimist-options": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "integrity": "sha1-+6TIGRM54T7PTWG+sD8HAQPz2VQ=", "dev": true, "requires": { "arrify": "^1.0.1", @@ -9359,7 +9350,7 @@ "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "dev": true, "requires": { "is-plain-object": "^2.0.4" @@ -9455,7 +9446,7 @@ "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=", "dev": true, "requires": { "arr-diff": "^4.0.0", @@ -9486,7 +9477,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", "dev": true } } @@ -9610,7 +9601,7 @@ } }, "ngFlowchart": { - "version": "git://github.com/thingsboard/ngFlowchart.git#ad172c26bb731f4e4e79d05dfa8cdc3f59cd1690", + "version": "git://github.com/thingsboard/ngFlowchart.git#1343a7478961f68280d81f0ecda4e722a2068e0f", "from": "git://github.com/thingsboard/ngFlowchart.git#master" }, "ngclipboard": { @@ -9670,7 +9661,7 @@ "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "integrity": "sha1-YLgTOWvjmz8SiKTB7V0efSi0ZKw=", "dev": true, "requires": { "lower-case": "^1.1.1" @@ -9689,7 +9680,7 @@ "node-fetch": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "integrity": "sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=", "requires": { "encoding": "^0.1.11", "is-stream": "^1.0.1" @@ -10116,7 +10107,7 @@ "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", "dev": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -10445,7 +10436,7 @@ "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -10707,7 +10698,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -10906,7 +10897,7 @@ "postcss-loader": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "integrity": "sha1-a5eUPkfHLYRfqeA/Jzdz1OjdbC0=", "dev": true, "requires": { "loader-utils": "^1.1.0", @@ -10918,7 +10909,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -10992,7 +10983,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "supports-color": { @@ -11068,7 +11059,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -11099,13 +11090,13 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -11131,7 +11122,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -11162,13 +11153,13 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -11189,7 +11180,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -11220,13 +11211,13 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -11246,7 +11237,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -11277,13 +11268,13 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -11378,7 +11369,7 @@ "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "integrity": "sha1-I4Hts2ifelPWUxkAYPz4ItLzaP8=", "dev": true }, "process": { @@ -11402,7 +11393,7 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", "requires": { "asap": "~2.0.3" } @@ -11482,7 +11473,7 @@ "pumpify": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "integrity": "sha1-NlE74karJ1cLGjdKXOJ4v9dDcM4=", "dev": true, "requires": { "duplexify": "^3.6.0", @@ -11560,7 +11551,7 @@ "ramda": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", - "integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==", + "integrity": "sha1-j99oIxz/qQvC+UYDkKDLdKKbKak=", "dev": true }, "randomatic": { @@ -11690,7 +11681,7 @@ "rc-menu": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-5.1.4.tgz", - "integrity": "sha512-ZUkUNda70GtTXcQDiO3rSDdk3sgIwDwzPUm5dVM8nRH/j84qv0BVBkIUwIBu8+s+G3G9lWLurRqh22dCqZPeOA==", + "integrity": "sha1-5d8I/ouDPoFGkTX/E7MKuPIf88Y=", "requires": { "babel-runtime": "6.x", "classnames": "2.x", @@ -11721,7 +11712,7 @@ "rc-trigger": { "version": "1.11.5", "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-1.11.5.tgz", - "integrity": "sha512-MBuUPw1nFzA4K7jQOwb7uvFaZFjXGd00EofUYiZ+l/fgKVq8wnLC0lkv36kwqM7vfKyftRo2sh7cWVpdPuNnnw==", + "integrity": "sha1-+I+fhODnn44O8cjRv4rCIItxViA=", "requires": { "babel-runtime": "6.x", "create-react-class": "15.x", @@ -11885,7 +11876,7 @@ "react-transition-group": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz", - "integrity": "sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==", + "integrity": "sha1-4R9yslf5IbITIpp3TfRmEjRsfKY=", "requires": { "chain-function": "^1.0.0", "dom-helpers": "^3.2.0", @@ -11897,7 +11888,7 @@ "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "integrity": "sha1-wAATh15Vexzw39mjaKHD2rO1SN0=", "requires": { "lodash": "^4.0.1" } @@ -11926,7 +11917,7 @@ "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -12276,12 +12267,12 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" }, "regex-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=", "dev": true, "requires": { "is-equal-shallow": "^0.1.3" @@ -12290,7 +12281,7 @@ "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", "dev": true, "requires": { "extend-shallow": "^3.0.2", @@ -12455,7 +12446,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "integrity": "sha1-iaf92TgmEmcxjq/hT5wy5ZjDaQk=", "dev": true }, "require-main-filename": { @@ -12548,7 +12539,7 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", "dev": true }, "retry": { @@ -12641,7 +12632,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" }, "sass-graph": { "version": "2.2.4", @@ -12693,7 +12684,7 @@ "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=", "dev": true, "requires": { "ajv": "^6.1.0", @@ -13005,7 +12996,7 @@ "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", "dev": true, "requires": { "base": "^0.11.1", @@ -13041,7 +13032,7 @@ "snapdragon-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", "dev": true, "requires": { "define-property": "^1.0.0", @@ -13061,7 +13052,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -13070,7 +13061,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -13079,7 +13070,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -13096,7 +13087,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", "dev": true } } @@ -13104,7 +13095,7 @@ "snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", "dev": true, "requires": { "kind-of": "^3.2.0" @@ -13113,7 +13104,7 @@ "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "integrity": "sha1-2Xa76ACve9IK4IWY1YI5NQiZPA0=", "dev": true, "requires": { "faye-websocket": "^0.10.0", @@ -13184,7 +13175,7 @@ "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=", "dev": true, "requires": { "atob": "^2.1.1", @@ -13237,7 +13228,7 @@ "spdx-expression-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -13331,7 +13322,7 @@ "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", "dev": true, "requires": { "extend-shallow": "^3.0.0" @@ -13478,7 +13469,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -13502,7 +13493,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, "requires": { "safe-buffer": "~5.1.0" @@ -13511,7 +13502,7 @@ "stringify-entities": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", - "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "integrity": "sha1-qYQX5Ucf0iez5F09sYYcEcr2aPc=", "dev": true, "requires": { "character-entities-html4": "^1.0.0", @@ -13633,7 +13624,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -13959,7 +13950,7 @@ "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "integrity": "sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=", "dev": true, "requires": { "pify": "^3.0.0" @@ -14033,7 +14024,7 @@ "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "integrity": "sha1-BE8aSdiEL/MHqta1Be0Xi9lQE00=", "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0" @@ -14042,7 +14033,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "strip-indent": { @@ -14054,7 +14045,7 @@ "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -14150,7 +14141,7 @@ "stylelint-webpack-plugin": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/stylelint-webpack-plugin/-/stylelint-webpack-plugin-0.10.5.tgz", - "integrity": "sha512-jtYx3aJ2qDMvBMswe5NRPTO7kJgAKafc6GilAkWDp/ewoAmnoxA6TsYMnIPtLECRLwXevaCPvlh2JEUMGZCoUQ==", + "integrity": "sha1-C24NNz/14DuqgZfr4PJiWYG9Jms=", "dev": true, "requires": { "arrify": "^1.0.1", @@ -14174,7 +14165,7 @@ "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", "dev": true, "requires": { "arr-flatten": "^1.1.0", @@ -14276,7 +14267,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", @@ -14287,7 +14278,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", "dev": true } } @@ -14295,7 +14286,7 @@ "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", "dev": true, "requires": { "array-unique": "^0.3.2", @@ -14354,7 +14345,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -14363,7 +14354,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -14372,7 +14363,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -14409,13 +14400,13 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", "dev": true }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", "dev": true, "requires": { "arr-diff": "^4.0.0", @@ -14447,7 +14438,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -14478,13 +14469,13 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -14506,7 +14497,7 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + "integrity": "sha1-wiaIrtTqs83C3+rLtWFmBWCgCAQ=" }, "table": { "version": "5.4.4", @@ -14741,7 +14732,7 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", "requires": { "os-tmpdir": "~1.0.2" } @@ -14764,7 +14755,7 @@ "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", "dev": true, "requires": { "define-property": "^2.0.2", @@ -14970,7 +14961,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true } } @@ -15120,7 +15111,7 @@ "unified": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", - "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "integrity": "sha1-f71jD3GRJtZ9QMZEt+P2FwNfbbo=", "dev": true, "requires": { "bail": "^1.0.0", @@ -15194,7 +15185,7 @@ "unist-util-stringify-position": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", - "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", + "integrity": "sha1-Pzf881EnncvKdICrWIm7ioMu4cY=", "dev": true }, "unist-util-visit": { @@ -15282,7 +15273,7 @@ "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", "dev": true, "requires": { "punycode": "^2.1.0" @@ -15291,7 +15282,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=", "dev": true } } @@ -15362,7 +15353,7 @@ "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=", "dev": true }, "util": { @@ -15474,7 +15465,7 @@ "vfile": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", - "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "integrity": "sha1-5i2OcrIOg8MkvGxnJ47ickiL+Eo=", "dev": true, "requires": { "is-buffer": "^1.1.4", @@ -16563,7 +16554,7 @@ "websocket-extensions": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "integrity": "sha1-XS/yKXcAPsaHpLhwc9+7rBRszyk=", "dev": true }, "whatwg-fetch": { @@ -16659,7 +16650,7 @@ "ws": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "integrity": "sha1-y9nm514J/F0skAFfIfDECHXg3VE=", "requires": { "options": ">=0.0.5", "ultron": "1.0.x" diff --git a/ui/package.json b/ui/package.json index d43c0dda2c..bb7c1b4979 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard", "private": true, - "version": "2.4.1", + "version": "2.4.2", "description": "ThingsBoard UI", "licenses": [ { @@ -65,6 +65,7 @@ "leaflet": "^1.5.1", "leaflet-polylinedecorator": "^1.6.0", "leaflet-providers": "^1.8.0", + "leaflet.markercluster": "^1.4.1", "material-steppers": "git://github.com/thingsboard/material-steppers.git#master", "material-ui": "^0.16.1", "material-ui-number-input": "^5.0.16", diff --git a/ui/pom.xml b/ui/pom.xml index f037ccfde6..63eff3aafa 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 2.4.2-SNAPSHOT + 2.4.2 thingsboard org.thingsboard diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js index 6ed2921824..17acde4a62 100644 --- a/ui/src/app/api/attribute.service.js +++ b/ui/src/app/api/attribute.service.js @@ -277,7 +277,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) } var deleteEntityTimeseriesPromise; if (deleteTimeseries.length) { - deleteEntityTimeseriesPromise = deleteEntityTimeseries(entityType, entityId, deleteTimeseries, config); + deleteEntityTimeseriesPromise = deleteEntityTimeseries(entityType, entityId, deleteTimeseries, config, true); } if (Object.keys(timeseriesData).length) { var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/' + timeseriesScope; @@ -331,8 +331,9 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) return deferred.promise; } - function deleteEntityTimeseries(entityType, entityId, timeseries, config) { + function deleteEntityTimeseries(entityType, entityId, timeseries, config, deleteAllDataForKeys) { config = config || {}; + deleteAllDataForKeys = deleteAllDataForKeys || false; var deferred = $q.defer(); var keys = ''; for (var i = 0; i < timeseries.length; i++) { @@ -341,7 +342,9 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) } keys += timeseries[i].key; } - var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/delete' + '?keys=' + keys; + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/delete' + + '?keys=' + keys + + '&deleteAllDataForKeys=' + deleteAllDataForKeys; $http.delete(url, config).then(function success() { deferred.resolve(); }, function fail() { @@ -350,4 +353,4 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) return deferred.promise; } -} \ No newline at end of file +} diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index f39615c933..5ce4fd7108 100644 --- a/ui/src/app/api/entity.service.js +++ b/ui/src/app/api/entity.service.js @@ -1130,19 +1130,12 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device let statisticalInfo = {}; let newEntity = { name: entityParameters.name, - type: entityParameters.type + type: entityParameters.type, + label: entityParameters.label }; - let promise; - switch (entityType) { - case types.entityType.device: - promise = deviceService.saveDevice(newEntity, config); - break; - case types.entityType.asset: - promise = assetService.saveAsset(newEntity, true, config); - break; - } + let saveEntityPromise = getEntitySavePromise(entityType, newEntity, config); - promise.then(function success(response) { + saveEntityPromise.then(function success(response) { saveEntityRelation(entityType, response.id, entityParameters, config).then(function success() { statisticalInfo.create = { entity: 1 @@ -1166,7 +1159,15 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device break; } findIdEntity.then(function success(response) { - saveEntityRelation(entityType, response.id, entityParameters, config).then(function success() { + let promises = []; + if(response.label !== entityParameters.label || response.type !== entityParameters.type){ + response.label = entityParameters.label; + response.type = entityParameters.type; + promises.push(getEntitySavePromise(entityType, response, config)); + } + promises.push(saveEntityRelation(entityType, response.id, entityParameters, config)); + + $q.all(promises).then(function success() { statisticalInfo.update = { entity: 1 }; @@ -1193,6 +1194,19 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return deferred.promise; } + function getEntitySavePromise(entityType, newEntity, config) { + let promise; + switch (entityType) { + case types.entityType.device: + promise = deviceService.saveDevice(newEntity, config); + break; + case types.entityType.asset: + promise = assetService.saveAsset(newEntity, true, config); + break; + } + return promise; + } + function getRelatedEntity(entityId, keys, typeTranslatePrefix) { var deferred = $q.defer(); getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then( diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index e89650c6a4..728af2d38f 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -132,6 +132,13 @@ export default class Subscription { } this.subscriptionTimewindow = null; + this.comparisonEnabled = options.comparisonEnabled; + if (this.comparisonEnabled) { + this.timeForComparison = options.timeForComparison; + + this.comparisonTimeWindow = {}; + this.timewindowForComparison = null; + } this.units = options.units || ''; this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2; @@ -311,13 +318,28 @@ export default class Subscription { } configureData() { + var additionalDatasources = []; var dataIndex = 0; + var additionalKeysNumber = 0; for (var i = 0; i < this.datasources.length; i++) { var datasource = this.datasources[i]; + var additionalDataKeys = []; + let datasourceAdditionalKeysNumber = 0; + for (var a = 0; a < datasource.dataKeys.length; a++) { var dataKey = datasource.dataKeys[a]; dataKey.hidden = false; dataKey.pattern = angular.copy(dataKey.label); + + if (this.comparisonEnabled && dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) { + datasourceAdditionalKeysNumber++; + additionalKeysNumber++; + let additionalDataKey = this.ctx.utils.createAdditionalDataKey(dataKey,datasource, this.timeForComparison,this.datasources,additionalKeysNumber); + dataKey.settings.comparisonSettings.color = additionalDataKey.color; + + additionalDataKeys.push(additionalDataKey); + } + var datasourceData = { datasource: datasource, dataKey: dataKey, @@ -341,8 +363,46 @@ export default class Subscription { this.legendData.data.push(legendKeyData); } } + + if (datasourceAdditionalKeysNumber > 0) { + let additionalDatasource = angular.copy(datasource); + additionalDatasource.dataKeys = additionalDataKeys; + additionalDatasource.isAdditional = true; + additionalDatasources.push(additionalDatasource); + } + } + + for (var j=0; j < additionalDatasources.length; j++) { + let additionalDatasource = additionalDatasources[j]; + for (var k=0; k < additionalDatasource.dataKeys.length; k++) { + let additionalDataKey = additionalDatasource.dataKeys[k]; + var additionalDatasourceData = { + datasource: additionalDatasource, + dataKey: additionalDataKey, + data: [] + }; + this.data.push(additionalDatasourceData); + this.hiddenData.push({data: []}); + if (this.displayLegend) { + var additionalLegendKey = { + dataKey: additionalDataKey, + dataIndex: dataIndex++ + }; + this.legendData.keys.push(additionalLegendKey); + var additionalLegendKeyData = { + min: null, + max: null, + avg: null, + total: null, + hidden: false + }; + this.legendData.data.push(additionalLegendKeyData); + } + } } + this.datasources = this.datasources.concat(additionalDatasources); + var subscription = this; var registration; @@ -638,7 +698,7 @@ export default class Subscription { updateTimewindow() { this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; if (this.subscriptionTimewindow.realtimeWindowMs) { - this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff; + this.timeWindow.maxTime = (moment()).valueOf() + this.timeWindow.stDiff;//eslint-disable-line this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; } else if (this.subscriptionTimewindow.fixedWindow) { this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; @@ -659,6 +719,26 @@ export default class Subscription { return this.subscriptionTimewindow; } + updateComparisonTimewindow() { + this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000; + if (this.timewindowForComparison.realtimeWindowMs) { + this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); //eslint-disable-line + this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs; + } else if (this.timewindowForComparison.fixedWindow) { + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs; + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs; + } + } + + updateSubscriptionForComparison() { + if (!this.subscriptionTimewindow) { + this.subscriptionTimewindow = this.updateRealtimeSubscription(); + } + this.timewindowForComparison = this.ctx.timeService.createTimewindowForComparison(this.subscriptionTimewindow, this.timeForComparison); + this.updateComparisonTimewindow(); + return this.timewindowForComparison; + } + dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { for (var x = 0; x < this.datasourceListeners.length; x++) { this.datasources[x].dataReceived = this.datasources[x].dataReceived === true; @@ -689,6 +769,9 @@ export default class Subscription { if (update) { if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { this.updateTimewindow(); + if (this.timewindowForComparison && this.timewindowForComparison.realtimeWindowMs) { + this.updateComparisonTimewindow(); + } } currentData.data = sourceData.data; if (this.caulculateLegendData) { @@ -745,6 +828,9 @@ export default class Subscription { this.notifyDataLoading(); if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { this.updateRealtimeSubscription(); + if (this.comparisonEnabled) { + this.updateSubscriptionForComparison(); + } if (this.subscriptionTimewindow.fixedWindow) { this.onDataUpdated(); } @@ -776,6 +862,17 @@ export default class Subscription { datasourceIndex: index }; + if (this.comparisonEnabled && datasource.isAdditional) { + listener.subscriptionTimewindow = this.timewindowForComparison; + listener.updateRealtimeSubscription = function () { + this.subscriptionTimewindow = subscription.updateSubscriptionForComparison(); + return this.subscriptionTimewindow; + }; + listener.setRealtimeSubscription = function () { + subscription.updateSubscriptionForComparison(); + }; + } + for (var a = 0; a < datasource.dataKeys.length; a++) { this.data[index + a].data = []; } @@ -908,7 +1005,6 @@ export default class Subscription { }); this.registrations = []; } - } function calculateMin(data) { diff --git a/ui/src/app/api/time.service.js b/ui/src/app/api/time.service.js index 878d43eed8..6b2e3f7687 100644 --- a/ui/src/app/api/time.service.js +++ b/ui/src/app/api/time.service.js @@ -47,6 +47,7 @@ function TimeService($translate, $http, $q, types) { defaultTimewindow: defaultTimewindow, toHistoryTimewindow: toHistoryTimewindow, createSubscriptionTimewindow: createSubscriptionTimewindow, + createTimewindowForComparison: createTimewindowForComparison, getMaxDatapointsLimit: function () { return maxDatapointsLimit; }, @@ -383,5 +384,27 @@ function TimeService($translate, $http, $q, types) { } } + function createTimewindowForComparison(subscriptionTimewindow, timeUnit) { + var timewindowForComparison = { + fixedWindow: null, + realtimeWindowMs: null, + aggregation: subscriptionTimewindow.aggregation + }; + + if (subscriptionTimewindow.realtimeWindowMs) { + timewindowForComparison.startTs = moment(subscriptionTimewindow.startTs).subtract(1, timeUnit).valueOf(); //eslint-disable-line + timewindowForComparison.realtimeWindowMs = subscriptionTimewindow.realtimeWindowMs; + } else if (subscriptionTimewindow.fixedWindow) { + var timeInterval = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; + var endTimeMs = moment(subscriptionTimewindow.fixedWindow.endTimeMs).subtract(1, timeUnit).valueOf(); //eslint-disable-line + timewindowForComparison.startTs = endTimeMs - timeInterval; + timewindowForComparison.fixedWindow = { + startTimeMs: timewindowForComparison.startTs, + endTimeMs: endTimeMs + }; + } + + return timewindowForComparison; + } } diff --git a/ui/src/app/asset/asset-fieldset.tpl.html b/ui/src/app/asset/asset-fieldset.tpl.html index cc5b14c3cc..1514c90d8a 100644 --- a/ui/src/app/asset/asset-fieldset.tpl.html +++ b/ui/src/app/asset/asset-fieldset.tpl.html @@ -63,6 +63,10 @@ ng-model="asset.type" entity-type="types.entityType.asset"> + + + + diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 37ccd91302..e6e65bbecf 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -369,6 +369,10 @@ export default angular.module('thingsboard.types', []) name: 'import.column-type.type', value: 'type' }, + label: { + name: 'import.column-type.label', + value: 'label' + }, clientAttribute: { name: 'import.column-type.client-attribute', value: 'CLIENT_ATTRIBUTE' diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index dd2a32de3e..d7820f65c4 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -144,6 +144,7 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t isLocalUrl: isLocalUrl, validateDatasources: validateDatasources, createKey: createKey, + createAdditionalDataKey: createAdditionalDataKey, createLabelFromDatasource: createLabelFromDatasource, insertVariable: insertVariable, customTranslation: customTranslation, @@ -411,8 +412,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t return copy; } - function genNextColor(datasources) { - var index = 0; + function genNextColor(datasources, initialIndex) { + var index = initialIndex || 0; if (datasources) { for (var i = 0; i < datasources.length; i++) { var datasource = datasources[i]; @@ -491,6 +492,23 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t return dataKey; } + function createAdditionalDataKey(dataKey, datasource, timeUnit, datasources, additionalKeysNumber) { + let additionalDataKey = angular.copy(dataKey); + if (dataKey.settings.comparisonSettings.comparisonValuesLabel) { + additionalDataKey.label = createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel); + } else { + additionalDataKey.label = dataKey.label + ' ' + $translate.instant('legend.comparison-time-ago.'+timeUnit); + } + additionalDataKey.pattern = additionalDataKey.label; + if (dataKey.settings.comparisonSettings.color) { + additionalDataKey.color = dataKey.settings.comparisonSettings.color; + } else { + additionalDataKey.color = genNextColor(datasources, additionalKeysNumber); + } + additionalDataKey._hash = Math.random(); + return additionalDataKey; + } + function createLabelFromDatasource(datasource, pattern) { var label = angular.copy(pattern); var match = varsRegex.exec(pattern); @@ -504,7 +522,7 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t } else if (variableName === 'deviceName') { label = label.split(variable).join(datasource.entityName); } else if (variableName === 'entityLabel') { - label = label.split(variable).join(datasource.entityLabel); + label = label.split(variable).join(datasource.entityLabel || datasource.entityName); } else if (variableName === 'aliasName') { label = label.split(variable).join(datasource.aliasName); } else if (variableName === 'entityDescription') { diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index d7e8b6247c..85ce18eb02 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -353,6 +353,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ ids.sort(function (id1, id2) { return id1.localeCompare(id2); }); + sortWidgets(); if (angular.equals(ids, vm.widgetIds)) { return; } @@ -388,7 +389,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ delete vm.widgetLayoutInfo[widgetId]; } } - sortWidgets(); $mdUtil.nextTick(function () { if (autofillHeight()) { updateMobileOpts(); diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js index afde04e2d2..9ed05b8812 100644 --- a/ui/src/app/components/datasource-entity.directive.js +++ b/ui/src/app/components/datasource-entity.directive.js @@ -165,7 +165,11 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc }; scope.transformAlarmDataKeyChip = function (chip) { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm}); + if (chip.type) { + return scope.generateDataKey({chip: chip.name, type: chip.type}); + } else { + return scope.generateDataKey({chip: chip, type: types.dataKeyType.alarm}); + } }; scope.showColorPicker = function (event, dataKey) { diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html index 9a58ecdb66..fe0d23eef7 100644 --- a/ui/src/app/components/datasource-entity.tpl.html +++ b/ui/src/app/components/datasource-entity.tpl.html @@ -125,7 +125,7 @@
entity.no-key-matching - entity.create-new-key + entity.create-new-key
diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js index 593c3de36e..5d98e44d0e 100644 --- a/ui/src/app/components/widget/widget.controller.js +++ b/ui/src/app/components/widget/widget.controller.js @@ -357,8 +357,10 @@ export default function WidgetController($scope, $state, $timeout, $window, $ocL if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { options = { type: widget.type, - stateData: vm.typeParameters.stateData - } + stateData: vm.typeParameters.stateData, + comparisonEnabled: widgetContext.settings.comparisonEnabled, + timeForComparison: widgetContext.settings.timeForComparison + }; if (widget.type == types.widgetType.alarm.value) { options.alarmSource = angular.copy(widget.config.alarmSource); options.alarmSearchStatus = angular.isDefined(widget.config.alarmSearchStatus) ? diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index f27be66e5f..5a77d95253 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -74,7 +74,6 @@ settings diff --git a/ui/src/app/import-export/import-dialog-csv.controller.js b/ui/src/app/import-export/import-dialog-csv.controller.js index a7b5329205..e11f59c5b4 100644 --- a/ui/src/app/import-export/import-dialog-csv.controller.js +++ b/ui/src/app/import-export/import-dialog-csv.controller.js @@ -98,7 +98,7 @@ export default function ImportDialogCsvController($scope, $mdDialog, toast, impo vm.columnsParam = []; var columnParam = {}; for (var i = 0; i < parseData.headers.length; i++) { - if (vm.importParameters.isHeader && parseData.headers[i].search(/^(name|type)$/im) === 0) { + if (vm.importParameters.isHeader && parseData.headers[i].search(/^(name|type|label)$/im) === 0) { columnParam = { type: types.importEntityColumnType[parseData.headers[i].toLowerCase()].value, key: parseData.headers[i].toLowerCase(), @@ -126,6 +126,7 @@ export default function ImportDialogCsvController($scope, $mdDialog, toast, impo var entityData = { name: "", type: "", + label: "", accessToken: "", attributes: { server: [], @@ -162,6 +163,9 @@ export default function ImportDialogCsvController($scope, $mdDialog, toast, impo case types.importEntityColumnType.type.value: entityData.type = importData.rows[i][j]; break; + case types.importEntityColumnType.label.value: + entityData.label = importData.rows[i][j]; + break; } } entitiesData.push(entityData); diff --git a/ui/src/app/import-export/table-columns-assignment.directive.js b/ui/src/app/import-export/table-columns-assignment.directive.js index a645e07392..5d96ba1c83 100644 --- a/ui/src/app/import-export/table-columns-assignment.directive.js +++ b/ui/src/app/import-export/table-columns-assignment.directive.js @@ -44,6 +44,7 @@ function TableColumnsAssignmentController($scope, types, $timeout) { vm.columnTypes.name = types.importEntityColumnType.name; vm.columnTypes.type = types.importEntityColumnType.type; + vm.columnTypes.label = types.importEntityColumnType.label; switch (vm.entityType) { case types.entityType.device: @@ -62,6 +63,7 @@ function TableColumnsAssignmentController($scope, types, $timeout) { if (newVal) { var isSelectName = false; var isSelectType = false; + var isSelectLabel = false; var isSelectCredentials = false; for (var i = 0; i < newVal.length; i++) { switch (newVal[i].type) { @@ -71,6 +73,9 @@ function TableColumnsAssignmentController($scope, types, $timeout) { case types.importEntityColumnType.type.value: isSelectType = true; break; + case types.importEntityColumnType.label.value: + isSelectLabel = true; + break; case types.importEntityColumnType.accessToken.value: isSelectCredentials = true; break; @@ -84,6 +89,7 @@ function TableColumnsAssignmentController($scope, types, $timeout) { $timeout(function () { vm.columnTypes.name.disable = isSelectName; vm.columnTypes.type.disable = isSelectType; + vm.columnTypes.label.disable = isSelectLabel; if (angular.isDefined(vm.columnTypes.accessToken)) { vm.columnTypes.accessToken.disable = isSelectCredentials; } diff --git a/ui/src/app/import-export/table-columns-assignment.tpl.html b/ui/src/app/import-export/table-columns-assignment.tpl.html index f00a08251c..b1eeafb8a7 100644 --- a/ui/src/app/import-export/table-columns-assignment.tpl.html +++ b/ui/src/app/import-export/table-columns-assignment.tpl.html @@ -41,6 +41,7 @@ Συνδεθείτε με διαφορετικό όνομα χρήστη, αν εξακολουθείτε να θέλετε να έχετε πρόσβαση σε αυτήν την τοποθεσία.", + "refresh-token-expired": "Η περίοδος χρήσης έχει λήξει", + "refresh-token-failed": "Δεν είναι δυνατή η ανανέωση της περιόδου χρήσης", + "permission-denied": "Άρνηση πρόσβασης!", + "permission-denied-text": "Δεν έχετε δικαίωμα εκτέλεσης αυτής της λειτουργίας!" + }, + "action": { + "activate": "Ενεργοποίηση", + "suspend": "Αναστολή", + "save": "Αποθήκευση", + "saveAs": "Αποθήκευση ως", + "cancel": "Ακύρωση", + "ok": "OK", + "delete": "Διαγραφή", + "add": "Προσθήκη", + "yes": "Ναι", + "no": "Όχι", + "update": "Ενημέρωση", + "remove": "Διαγραφή", + "search": "Αναζήτηση", + "clear-search": "Καθαρισμός Αναζήτησης", + "assign": "Ανάθεση", + "unassign": "Ακύρωση Ανάθεσης", + "share": "Διαμοιρασμός", + "make-private": "Ιδιωτικό", + "apply": "Εφαρμογή", + "apply-changes": "Εφααρμογή Αλλαγών", + "edit-mode": "Λειτουργία Επεξεργασίας", + "enter-edit-mode": "Έναρξη Επεξεργασίας", + "decline-changes": "Απόρριψη Αλλαγών", + "close": "Κλείσιμο", + "back": "Πίσω", + "run": "Εκτέλεση", + "sign-in": "Σύνδεση!", + "edit": "Επεξεργασία", + "view": "Επισκόπηση", + "create": "Δημιουργία", + "drag": "Drag", + "refresh": "Ανανέωση", + "undo": "Αναίρεση", + "copy": "Αντιγραφή", + "paste": "Επικόλληση", + "copy-reference": "Αντιγραφή παραπομπής", + "paste-reference": "Επικόλληση παραπομπής", + "import": "Εισαγωγή", + "export": "Εξαγωγή", + "share-via": "Διαμοίραση μέσω {{provider}}", + "move": "Μετακίνηση", + "select": "Επιλογή", + "continue": "Συνέχεια" + }, + "aggregation": { + "aggregation": "Συνάθροιση", + "function": "Συνάρτηση συνάθροισης δεδομένων", + "limit": "Μέγιστες τιμές", + "group-interval": "Διάστημα ομαδοποίησης", + "min": "Min", + "max": "Max", + "avg": "Μέσος Όρος", + "sum": "Άθροισμα", + "count": "Καταμέτρηση", + "none": "Κανένα" + }, + "admin": { + "general": "Γενικά", + "general-settings": "Γενικές Ρυθμίσεις", + "outgoing-mail": "Διακομιστής Αλληλογραφίας", + "outgoing-mail-settings": "Διακομιστής Εξερχόμενης Αλληλογραφίας", + "system-settings": "Ρυθμίσεις Συστήματος", + "test-mail-sent": "Το δοκιμαστικό μήνυμα στάλθηκε με επιτυχία!", + "base-url": "Βασική διεύθυνση URL", + "base-url-required": "Απαιτείται ορισμός Base URL.", + "mail-from": "Αποστολέας", + "mail-from-required": "Απαιτείται βασική διεύθυνση URL.", + "smtp-protocol": "Πρωτόκολλο SMTP", + "smtp-host": "SMTP host", + "smtp-host-required": "Απαιτείται ορισμός SMTP host.", + "smtp-port": "Θύρα SMTP", + "smtp-port-required": "Πρέπει να εισάγετε SMTP port.", + "smtp-port-invalid": "Αυτή δε φαίνεται να είναι μια έγκυρη SMTP port.", + "timeout-msec": "Timeout (msec)", + "timeout-required": "Απαιτείται τιμή Timeout.", + "timeout-invalid": "Αυτή δε φαίνεται να είναι μια έγκυρη τιμή timeout.", + "enable-tls": "Ενεργοποίηση TLS", + "send-test-mail": "Αποστολή δοκιμαστικού μηνύματος", + "use-system-mail-settings": "Χρήση των ρυθμίσεων διακομιστή αλληλογραφίας συστήματος", + "mail-templates": "Πρότυπα αλληλογραφίας", + "mail-template-settings": "Ρυθμίσεις προτύπων αλληλογραφίας", + "use-system-mail-template-settings": "Χρήση προτύπων ηλεκτρονικού ταχυδρομείου συστήματος", + "mail-template": { + "mail-template": "Πρότυπο αλληλογραφίας", + "test": "Δοκιμαστικό μήνυμα ηλεκτρονικού ταχυδρομείου", + "activation": "Μήνυμα ενεργοποίησης λογαριασμού", + "account-activated": "Μήνυμα επιτυχούς ενεργοποίησης λογαριασμού", + "reset-password": "Μήνυμα επαναφοράς κωδικού πρόσβασης", + "password-was-reset": "Μήνυμα επαναφοράς κωδικού πρόσβασης", + "user-activated": "Μήνυμα ενεργοποίησης χρήστη", + "user-registered": "Μήνυμα καταχώρησης χρήστη" + }, + "mail-subject": "Θέμα μηνύματος (subject)", + "mail-body": "Κυρίως μήνυμα (body)" + }, + "alarm": { + "alarm": "Alarm", + "alarms": "Alarms", + "select-alarm": "Επιλογή alarm", + "no-alarms-matching": "Δεν βρέθηκαν alarms σχετικά με '{{entity}}'.", + "alarm-required": "Απαιτείται Alarm", + "alarm-status": "Κατάσταση Alarm", + "search-status": { + "ANY": "'Ολες", + "ACTIVE": "Ενεργό", + "CLEARED": "Εκκαθαρίστηκε", + "ACK": "Επιβεβαιώθηκε", + "UNACK": "Χωρίς επιβεβαίωση" + }, + "display-status": { + "ACTIVE_UNACK": "Ενεργό χωρίς επιβεβαίωση", + "ACTIVE_ACK": "Ενεργό επιβεβαιωμένο", + "CLEARED_UNACK": "Εκκαθαρίστηκε χωρίς επιβεβαίωση", + "CLEARED_ACK": "Εκκαθαρίστηκε επιβεβαιωμένο" + }, + "no-alarms-prompt": "Δεν βρέθηκαν alarm", + "created-time": "Ώρα δημιουργίας", + "type": "Τύπος", + "severity": "Βαρύτητα", + "originator": "Δημιουργός", + "originator-type": "Τύπος δημιουργού", + "details": "Λεπτομέρειες", + "status": "Κατάσταση", + "alarm-details": "Λεπτομέρειες Alarm", + "start-time": "Ώρα έναρξης", + "end-time": "Ώρα λήξης", + "ack-time": "Ώρα επιβεβαίωσης", + "clear-time": "Ώρα εκκαθάρισης", + "severity-critical": "Critical", + "severity-major": "Κρίσιμο", + "severity-minor": "Ασήμαντο", + "severity-warning": "Προειδοποίηση", + "severity-indeterminate": "Απροσδιόριστο", + "acknowledge": "Επιβεβαίωση", + "clear": "Εκκαθάριση", + "search": "Αναζήτηση alarm", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } επιλέχθηκαν", + "no-data": "Δεν υπάρχουν δεδομένα για εμφάνιση", + "polling-interval": "Διάστημα δειγματοληψίας alarm(sec)", + "polling-interval-required": "Απαιτείται ορισμός διαστήματος δειγματοληψίας alarm.", + "min-polling-interval-message": "Η ελάχιστη επιτρεπόμενη τιμή διαστήματος δειγματοληψίας alarm είναι 1 sec.", + "aknowledge-alarms-title": "Επιβεβαίωση { count, plural, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "Είστε σίγουρος ότι θέλετε να επιβεβαιώσετε { count, plural, 1 {1 alarm} other {# alarms} };", + "aknowledge-alarm-title": "Επιβεβαίωση Alarm", + "aknowledge-alarm-text": "Είστε σίγουρος ότι θέλετε να επιβεβαιώσετε το Alarm?", + "clear-alarms-title": "Εκκαθάριση { count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "Είστε σίγουρος ότι θέλετε να εκκαθαρίσετε { count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarm-title": "Εκκαθάριση Alarm", + "clear-alarm-text": "Είστε σίγουρος ότι θέλετε να εκκαθαρίσετε το Alarm?", + "alarm-status-filter": "Φίλτρο κατάστασης Alarm" + }, + "alias": { + "add": "Προσθήκη ψευδωνύμου", + "edit": "Επεξεργασία ψευδωνύμου", + "name": "Ψευδώνυμο", + "name-required": "Απαιτείται Ψευδώνυμο", + "duplicate-alias": "Το ψευδώνυμο υπάρχει ήδη.", + "filter-type-single-entity": "Απλή Οντότητα", + "filter-type-entity-group": "Ομάδα Οντοτήτων", + "filter-type-entity-list": "Λίστα Οντοτήτων", + "filter-type-entity-name": "Όνομα Οντότητας", + "filter-type-entity-group-list": "Λίστα ομάδας Οντοτήτων", + "filter-type-entity-group-name": "Όνομα ομάδας Οντοτήτων", + "filter-type-state-entity": "Οντότητα από την κατάσταση του dashboard", + "filter-type-state-entity-description": "Οντότητα που λαμβάνεται από τις παραμέτρους κατάστασης του dashboard", + "filter-type-asset-type": "Τύπος Asset", + "filter-type-asset-type-description": "Assets του τύπου '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Assets του τύπου '{{assetType}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-device-type": "Τύπος Συσκευής", + "filter-type-device-type-description": "Συσκευές του τύπου '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Συσκευές του τύπου '{{deviceType}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-entity-view-type": "Τύπος προβολής Οντοτήτων", + "filter-type-entity-view-type-description": "Τύπος προβολής Οντοτήτων '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Τύπος προβολής Οντοτήτων '{{entityView}}' με όνομα που αρχίζει από '{{prefix}}'", + "filter-type-relations-query": "Ερώτημα Σχέσεων", + "filter-type-relations-query-description": "{{entities}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Ερώτημα αναζήτησης Asset", + "filter-type-asset-search-query-description": "Asset με τύπο {{assetTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Ερώτημα αναζήτησης Συσκευών", + "filter-type-device-search-query-description": "Συσκευές με τύπο {{deviceTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Ερώτημα αναζήτησης Όψεων Οντοτήτων", + "filter-type-entity-view-search-query-description": "Όψεις Οντοτήτων με τύπο {{entityViewTypes}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}", + "entity-filter": "Φίλτρο Οντοτήτων", + "resolve-multiple": "Διακανονισμός ως πολλαπλές οντότητες", + "filter-type": "Είδος Φίλτρου", + "filter-type-required": "Απαιτείται καθορισμός του Είδους Φίλτρου.", + "entity-filter-no-entity-matched": "Δεν βρέθηκαν οντότητες που ταιριάζουν με το συγκεκριμένο φίλτρο.", + "no-entity-filter-specified": "Δεν έχει οριστεί Φίλτρο Οντοτήτων", + "root-state-entity": "Χρήση της οντότητας κατάστασης του dashboard ως ρίζα", + "group-state-entity": "Χρήση της οντότητας κατάστασης του dashboard ως ομάδας οντοτήτων", + "root-entity": "Οντότητα ρίζας", + "state-entity-parameter-name": "Όνομα παραμέτρου οντότητας κατάστασης", + "default-state-entity": "Προεπιλεγμένη οντότητα κατάστασης", + "default-state-entity-group": "Προεπιλεγμένη ομάδα καταστάσεων οντοτήτων", + "default-entity-parameter-name": "Εξ ορισμού", + "max-relation-level": "Μέγιστος βαθμός συσχέτισης", + "unlimited-level": "Απεριόριστος βαθμός", + "state-entity": "Οντότητα κατάστασης του Dashboard", + "entities-of-group-state-entity": "Οντότητες από την ομάδα οντοτήτων κατάστασης του dashboard", + "all-entities": "Όλες οι Οντότητες", + "any-relation": "Οποιαδήποτε" + }, + "asset": { + "asset": "Asset", + "assets": "Assets", + "management": "Διαχείριση Asset", + "view-assets": "Προβολή Asset", + "add": "Προσθήκη Asset", + "assign-to-customer": "Ανάθεση σε Πελάτη", + "assign-asset-to-customer": "Ανάθεση Asset(s) σε Πελάτη", + "assign-asset-to-customer-text": "Επιλέξτε τα Asset που θα αναθέσετε στον πελάτη", + "no-assets-text": "Δεν βρέθηκαν Asset", + "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να αναθέσετε το Asset(s)", + "public": "Δημόσιο", + "assignedToCustomer": "Συνδεδεμένο με πελάτη", + "make-public": "Κάνε το Asset δημόσιο", + "make-private": "Κάνε το Asset ιδιωτικό", + "unassign-from-customer": "Αποσύνδεση από τον πελάτη", + "delete": "Διαγραφή Αsset", + "asset-public": "Το Asset είναι δημόσιο", + "asset-type": "Τύπος Asset", + "asset-type-required": "Απαιτείται ορισμός είδους Asset.", + "select-asset-type": "Επιλογή τύπου Αsset", + "enter-asset-type": "Εισαγωγή τύπου Αsset", + "any-asset": "Οποιοδήποτε Αsset", + "no-asset-types-matching": "Δεν βρέθηκαν τύποι Asset που να τιαιριάζουν με '{{entitySubtype}}'.", + "asset-type-list-empty": "Δεν επιλέχθηκε τύπος Asset.", + "asset-types": "Τύποι Asset", + "name": "Όνομα", + "name-required": "Απαιτείται Όνομα.", + "description": "Περιγραφή", + "type": "Τύπος", + "type-required": "Απαιτείται Τύπος.", + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "add-asset-text": "Προσθήκη νέου Asset", + "asset-details": "Λεπτομέρειες Asset", + "assign-assets": "Ανάθεση Assets", + "assign-assets-text": "Ανάθεση { count, plural, 1 {1 asset} other {# assets} } σε πελάτη", + "delete-assets": "Διαγραφή Assets", + "unassign-assets": "Αποσύνδεση Assets", + "unassign-assets-action-title": "Αποσύνδεση { count, plural, 1 {1 asset} other {# assets} } από πελάτη", + "assign-new-asset": "Ανάθεση νέου Asset", + "delete-asset-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το Asset '{{assetName}}'?", + "delete-asset-text": "Προσοχή!, Μετά την επιβεβαίωση, το Asset και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-assets-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 asset} other {# assets} };", + "delete-assets-action-title": "Διαγραφή { count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "Προσοχή! Μετά την επιβεβαίωση, όλα τα επιλεγμένα Asset και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "make-public-asset-title": "Είστε βέβαιοι ότι θέλετε να κάνετε το Asset '{{assetName}}' δημόσιο;", + "make-public-asset-text": "Μετά την επιβεβαίωση, το Asset και όλα τα στοιχεία του θα είναι προσβάσιμα από άλλους.", + "make-private-asset-title": "Είστε βέβαιοι ότι θέλετε να κάνετε το Asset '{{assetName}}' ιδιωτικό;", + "make-private-asset-text": "Μετά την επιβεβαίωση, το Asset και όλα τα στοιχεία του θα είναι ιδιωτικά και μη προσβάσιμα από άλλους.", + "unassign-asset-title": "Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε το Asset '{{assetName}}';", + "unassign-asset-text": "Μετά την επιβεβαίωση, το Asset θα αποσυνδεθεί και δεν θα είναι προσβάσιμο από τον πελάτη.", + "unassign-asset": "Αποσύνδεση Asset", + "unassign-assets-title": "Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε { count, plural, 1 {1 asset} other {# assets} };", + "unassign-assets-text": "Μετά την επιβεβαίωση, όλα τα επιλεγμένα Asset θα αποσυνδεθούν και δεν θα είναι προσβάσιμα από τον πελάτη.", + "copyId": "Αντιγραφή Asset Id", + "idCopiedMessage": "Το Asset Id αντιγράφηκε στο πρόχειρο", + "select-asset": "Επιλογή Asset", + "no-assets-matching": "Δεν βρέθηκαν Asset που να ταιριάζουν με '{{entity}}'.", + "asset-required": "Απαιτείται Asset", + "name-starts-with": "Όνομα Asset που ξεκινάει με", + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } επιλέχθηκαν", + "search": "Αναζήτηση Asset", + "select-group-to-add": "Επιλέξτε ομάδα στόχο για να προσθέσετε επιλεγμένα Asset", + "select-group-to-move": "Επιλέξτε ομάδα στόχο για να μετακινήσετε επιλεγμένα assets", + "remove-assets-from-group": "Είστε βέβαιοι ότι θέλετε να καταργήσετε { count, plural, 1 {1 asset} other {# assets} } από την ομάδα '{entityGroup}';", + "group": "Ομάδα Asset", + "list-of-groups": "{ count, plural, 1 {One asset group} other {List of # asset groups} }", + "group-name-starts-with": "Ομάδες Asset των οποίων τα ονόματα ξεκινούν με '{{prefix}}'", + "import": "Εισαγωγή Asset", + "asset-file": "Αρχείο Asset" + }, + "attribute": { + "attributes": "Χαρακτηριστικά", + "latest-telemetry": "Τελευταία τηλεμετρία", + "attributes-scope": "Πεδίο εφαρμογής Χαρακτηριστικών Οντότητας", + "scope-latest-telemetry": "Τελευταία τηλεμετρία", + "scope-client": "Χαρακτηριστικά Client", + "scope-server": "Χαρακτηριστικά Server", + "scope-shared": "Κοινόχρηστα Χαρακτηριστικά", + "add": "Προσθήκη Χαρακτηριστικού", + "add-attribute-prompt": "Προσθέστε Χαρακτηριστικό", + "key": "Όνομα", + "last-update-time": "Ώρα τελευταίας ενημέρωσης", + "key-required": "Απαιτείται μεταβλητή χαρακτηριστικού.", + "value": "Τιμή", + "value-required": "Απαιτείται ορισμός τιμής χαρακτηριστικού.", + "delete-attributes-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 attribute} other {# attributes} };", + "delete-attributes-text": "Προσοχή! Μετά την επιβεβαίωση θα αφαιρεθούν όλα τα επιλεγμένα χαρακτηριστικά.", + "delete-attributes": "Διαγραφή χαρακτηριστικών", + "enter-attribute-value": "Καταχωρίστε την τιμή του χαρακτηριστικού", + "show-on-widget": "Εμφάνιση σε widget", + "widget-mode": "Είδος λειτουργίας widget", + "next-widget": "Επόμενο widget", + "prev-widget": "Προηγούμενο widget", + "add-to-dashboard": "Προσθήκη σε dashboard", + "add-widget-to-dashboard": "Προσθήκη widget στο dashboard", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } επιλέχθηκαν", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } επιλέχθηκαν" + }, + "audit-log": { + "audit": "Καταγραφή", + "audit-logs": "Ημερολόγια Καταγραφής", + "timestamp": "Ώρα", + "entity-type": "Είδος Οντότητας", + "entity-name": "Όνομα Οντότητας", + "user": "Χρήστης", + "type": "Τύπος", + "status": "Κατάσταση", + "details": "Λεπτομέρειες", + "type-added": "Προστέθηκε", + "type-deleted": "Διαγράφηκε", + "type-updated": "Ενημερώθηκε", + "type-attributes-updated": "Τα χαρακτηριστικά ενημερώθηκαν", + "type-attributes-deleted": "Τα χαρακτηριστικά διαγράφηκαν", + "type-rpc-call": "Κλήση RPC", + "type-credentials-updated": "Τα διαπιστευτήρια ενημερώθηκαν", + "type-assigned-to-customer": "Ανατέθηκε σε Πελάτη", + "type-unassigned-from-customer": "Αποσυνδέθηκε από Πελάτη", + "type-activated": "Ενεργοποιήθηκε", + "type-suspended": "Μπήκε σε αναστολή", + "type-credentials-read": "Τα διαπιστευτήρια διαβάστηκαν", + "type-attributes-read": "Τα χαρακτηριστικά διαβάστηκαν", + "type-added-to-entity-group": "Προστέθηκε στην ομάδα", + "type-removed-from-entity-group": "Αφαιρέθηκε από την ομάδα", + "type-relation-add-or-update": "Relation updated", + "type-relation-delete": "Η συσχέτιση ενημερώθηκε", + "type-relations-delete": "Όλες οι συσχετίσεις διαγράφηκαν", + "type-alarm-ack": "Επιβεβαιώθηκε", + "type-alarm-clear": "Εκκαθαρίστηκε", + "type-rest-api-rule-engine-call": "Κλήση Rule engine REST API", + "type-made-public": "Έγινε δημόσιο", + "type-made-private": "Έγινε ιδιωτικό", + "status-success": "Επιτυχία", + "status-failure": "Αποτυχία", + "audit-log-details": "΄Λεπτομέρειες καταγραφής", + "no-audit-logs-prompt": "Δεν βρέθηκαν αρχεία καταγραφής", + "action-data": "Δεδομένα ενεργειών", + "failure-details": "Λεπτομέρειες αποτυχίας", + "search": "Αναζήτηση αρχείων καταγραφής", + "clear-search": "΄Καθαρισμός αναζήτησης" + }, + "confirm-on-exit": { + "message": "Έχετε μη αποθηκευμένες αλλαγές. Είστε βέβαιοι ότι θέλετε να φύγετε από τη σελίδα;", + "html-message": "Έχετε μη αποθηκευμένες αλλαγές.
Είστε βέβαιοι ότι θέλετε να φύγετε από τη σελίδα;", + "title": "Μη αποθηκευμένες αλλαγές" + }, + "contact": { + "country": "Χώρα", + "city": "Πόλη", + "state": "Περιφέρεια", + "postal-code": "Ταχυδρομικός Κώδικας", + "postal-code-invalid": "Μη έγκυρη μορφή ταχυδρομικού κώδικα.", + "address": "Διεύθυνση (γραμμή 1)", + "address2": "Διεύθυνση (γραμμή 2)", + "phone": "Τηλέφωνο", + "email": "Email", + "no-address": "Χωρίς διεύθυνση" + }, + "common": { + "username": "Όνομα χρήστη", + "password": "Κωδικός πρόσβασης", + "enter-username": "Εισάγετε Όνομα χρήστη", + "enter-password": "Εισάγετε Κωδικό πρόσβασης", + "enter-search": "Αναζήτηση" + }, + "converter": { + "converter": "Μετατροπέας δεδομένων", + "converters": "Μετατροπείς δεδομένων", + "select-converter": "Επιλογή μετατροπέα δεδομένων", + "no-converters-matching": "Δεν βρέθηκαν μετατροπείς δεδομένων για '{{entity}}'.", + "converter-required": "Απαιτείται ορισμός μετατροπέα δεδομένων", + "delete": "Διαγραφή μετατροπέα", + "management": "Διαχείριση μετατροπέων δεδομένων", + "add-converter-text": "Προσθήκη νέου μετατροπέα δεδομένων", + "no-converters-text": "Δεν βρέθηκαν μετατροπείς δεδομένων", + "selected-converters": "{ count, plural, 1 {1 data converter} other {# data converters} } επιλέχθηκαν", + "delete-converter-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον μετατροπέα δεδομένων '{{converterName}}'?", + "delete-converter-text": "Προσέξτε, μετά την επιβεβαίωση, ο μετατροπέας δεδομένων και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-converters-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 data converter} other {# data converters} };", + "delete-converters-action-title": "Διαγραφή { count, plural, 1 {1 data converter} other {# data converters} }", + "delete-converters-text": "Προσοχή! Μετά την επιβεβαίωση, όλοι οι επιλεγμένοι μετατροπείς δεδομένων και όλα τα σχετικά δεδομένα θα καταστούν μη ανακτήσιμα.", + "events": "Γεγονότα", + "add": "Προσθήκη μετατροπέα δεδομένων", + "converter-details": "Λεπτομέρειες μετατροπέα δεδομένων", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ID μετατροπέα δεδομένων", + "idCopiedMessage": "Το ID του μετατροπέα δεδομένων αντιγράφηκε στο πρόχειρο.", + "debug-mode": "Λειτουργία εντοπισμού σφαλμάτων", + "name": "Όνομα", + "name-required": "Απαιτείται Όνομα.", + "description": "Περιγραφή", + "decoder": "Αποκωδικοποιητής", + "encoder": "Kωδικοποιητής", + "test-decoder-fuction": "Δοκιμή λειτουργίας αποκωδικοποιητή", + "test-encoder-fuction": "Δοκιμή λειτουργίας κωδικοποιητή", + "decoder-input-params": "Είσοδο παραμέτρων αποκωδικοποιητή", + "encoder-input-params": "Είσοδο παραμέτρων κωδικοποιητή", + "payload": "Φορτίο", + "payload-content-type": "Τύπος περιεχόμενου φορτίου", + "payload-content": "Περιεχόμενο φορτίο", + "message": "Μήνυμα", + "message-type": "Τύπος μηνύματος", + "message-type-required": "Απαιτείται τύπος μηνύματος", + "test": "Δοκιμή", + "metadata": "Μεταδεδομένα", + "metadata-required": "Οι καταχωρίσεις μεταδεδομένων δεν μπορούν να είναι κενές.", + "integration-metadata": "Μεταδεδομένα ενσωμάτωσης", + "integration-metadata-required": "Οι καταχωρήσεις μεταδεδομένων ενσωμάτωσης δεν μπορούν να είναι κενές.", + "output": "Έξοδος", + "import": "Εισαγωγή μετατροπέα", + "export": "Εξαγωγή μετατροπέα", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή του μετατροπέα: {{error}}", + "create-new-converter": "Δημιουργία νέου μετατροπέα", + "converter-file": "Αρχείο μετατροπέα", + "invalid-converter-file-error": "Δεν είναι δυνατή η εισαγωγή του μετατροπέα: Μη έγκυρη δομή δεδομένων μετατροπέα.", + "type": "Τύπος", + "type-required": "Απαιτείται τύπος.", + "type-uplink": "Uplink", + "type-downlink": "Downlink" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binary (Base64)" + }, + "customer": { + "customer": "Πελάτης", + "customers": "Πελάτες", + "management": "Διαχείριση Πελατών", + "dashboard": "Dashboard Πελάτη", + "dashboards": "Dashboards Πελάτη", + "devices": "Συσκευές Πελάτη", + "entity-views": "Προβολές Οντοτήτων ΠελάτηCustomer Entity Views", + "assets": "Assets Πελάτη", + "public-dashboards": "Δημόσια Dashboards", + "public-devices": "Δημόσιες Devices", + "public-assets": "Δημόσια Assets", + "public-entity-views": "Δημόσιες Προβολές Οντοτήτων", + "add": "Προσθήκη Πελάτη", + "delete": "Διαγραφή πελάτη", + "manage-customer-user-groups": "Διαχείριση ομάδων χρηστών πελατών", + "manage-customer-groups": "Διαχείριση ομάδων πελατών", + "manage-customer-device-groups": "Διαχείριση ομάδων συσκευών πελατών", + "manage-customer-asset-groups": "Διαχείριση ομάδων Asset πελατών", + "manage-customer-entity-view-groups": "Διαχείριση ομάδων προβολής οντότητας πελατών", + "manage-customer-dashboard-groups": "Διαχείριση ομάδων dashboard πελατών", + "manage-customer-users": "Διαχείριση χρηστών πελατών", + "manage-customers": "Διαχείριση πελατών", + "manage-customer-devices": "Διαχείρηση συσκευών πελατών", + "manage-customer-entity-views": "Διαχείριση προβολής οντότητας πελατών", + "manage-customer-dashboards": "Διαχείριση dashboards πελατών", + "manage-public-devices": "Διαχείριση δημόσιων συσκευών", + "manage-public-dashboards": "Διαχείριση δημόσιων συσκευών", + "manage-customer-assets": "Διαχείριση οντοτήτων πελατών", + "manage-public-assets": "Διαχείριση δημόσιων οντοτήτων", + "add-customer-text": "Προσθήκη νέου πελάτη", + "no-customers-text": "Δεν βρέθηκαν πελάτες", + "customer-details": "Λεπτομέρειες πελάτη", + "delete-customer-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τον πελάτη '{{customerTitle}}'?", + "delete-customer-text": "Προσέξτε, μετά την επιβεβαίωση, ο πελάτης και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-customers-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "Διαγραφή { count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "Προσέξτε, μετά την επιβεβαίωση, οι πελάτες και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "manage-user-groups": "Διαχείρηση ομαδών χρηστών", + "manage-asset-groups": "Διαχείρηση ομαδών οντοτήτων", + "manage-device-groups": "Διαχείρηση ομαδών συσκευών", + "manage-dashboard-groups": "Διαχείρηση ομαδών dashboards", + "manage-entity-view-groups": "Διαχείρηση ομαδών προβολής οντοτήτων", + "manage-users": "Διαχείρηση χρηστών", + "manage-assets": "Διαχείρηση οντοτήτων", + "manage-devices": "Διαχείρηση συσκευών", + "manage-dashboards": "Διαχείρηση dashboards", + "title": "Τίτλος", + "title-required": "Απαιτείται ένας τίτλος.", + "description": "Περιγραφή", + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "copyId": "Αντιγραφή ID πελάτη", + "idCopiedMessage": "Το ID του πελάτη έχει αντιγραφεί στο πρόχειρο", + "select-customer": "Επιλέξτε πελάτη", + "no-customers-matching": "Δεν βρέθηκαν πελάτες που να αντιστοιχούν σε '{{entity}}'.", + "customer-required": "Απαιτείται πελάτης", + "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } επιλέχθηκαν", + "search": "Αναζήτηση πελατών", + "select-group-to-add": "Επιλέξτε ομάδα για να προσθέσετε τους επιλεγμένους πελάτες", + "select-group-to-move": "Επιλέξτε ομάδα για να μετακινήσετε τους επιλεγμένους πελάτες", + "remove-customers-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 customer} other {# customers} } από την ομάδα '{entityGroup}'?", + "group": "Ομάδα πελατών", + "list-of-groups": "{ count, plural, 1 {One customer group} other {List of # customer groups} }", + "group-name-starts-with": "Πελάτες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "select-default-customer": "Επιλογή προεπιλεγμένου πελάτη", + "default-customer": "Προεπιλεγμένος πελάτης", + "default-customer-required": "Ο προεπιλεγμένος πελάτης είναι υποχρεωτικός για να είναι δυνατή η απασφαλμάτωση του dashboard από τον Tenant", + "allow-white-labeling": "Επιτρέψτε το White Labeling" + }, + "customers-hierarchy": { + "customers-hierarchy": "Ιεραρχία Πελατών", + "open-nav-tree": "Άνοιγμα διακλάδωσης", + "return-to-top-level": "Επιστροφή στο αρχικό επίπεδο" + }, + "custom-menu": { + "custom-menu": "Προσαρμοσμένο Μενού", + "custom-menu-hint": "Ορίστε το προσαρμοσμένο μενού (JSON) παρακάτω. Αυτό το JSON περιέχει μια λίστα με προσαρμοσμένα στοιχεία μενού." + }, + "custom-translation": { + "custom-translation": "Μεταφράσεις", + "translation-map": "Χάρτης μετάφρασης", + "key": "Κλειδί", + "import": "Εισαγωγή μετάφρασης", + "export": "Εξαγωγή μετάφρασης", + "export-data": "Εξαγωγή δεδομένων μετάφρασης", + "import-data": "Εισαγωγή δεδομένων μετάφρασης", + "translation-file": "Αρχείο μετάφρασης", + "invalid-translation-file-error": "Δεν είναι δυνατή η εισαγωγή αρχείου μετάφρασης: Μη έγκυρη δομή δεδομένων μετάφρασης.", + "custom-translation-hint": "Καθορίστε την προσαρμοσμένη μετάφραση (JSON) παρακάτω. Αυτό το JSON θα αντικαταστήσει την προεπιλεγμένη μετάφραση. Κάνετε Λήψη αρχείου γλώσσας για να λάβετε την υπάρχουσα μετάφραση. Μπορείτε επίσης να χρησιμοποιήσετε το ληφθέν αρχείο ως αναφορά στα διαθέσιμα ζεύγη κλειδιών-τιμών μετάφρασης.", + "download-locale-file": "Λήψη αρχείου γλώσσας" + }, + "datetime": { + "date-from": "Ημ/νία από", + "time-from": "Ώρα από", + "date-to": "Ημ/νία έως", + "time-to": "Ώρα έως" + }, + "dashboard": { + "dashboard": "Dashboard", + "dashboards": "Dashboards", + "management": "Διαχείριση Dashboard", + "view-dashboards": "Προβολή Dashboards", + "add": "Προσθήκη Dashboard", + "assign-dashboard-to-customer": "Ανάθεση Dashboard(s) Σε Πελάτη", + "assign-dashboard-to-customer-text": "Παρακαλώ επιλέξτε τα dashboards που θέλετε να αναθέσετε σε πελάτη", + "assign-to-customer-text": "Παρακαλώ επιλέξτε τον πελάτη στον οποίο θέλετε να αναθέσετε το/τα dashboard(s)", + "assign-to-customer": "Ανάθεση σε πελάτη", + "unassign-from-customer": "Αφαίρεση από πελάτη", + "make-public": "Δημοσιοποιήση Dashboard", + "make-private": "Ιδιωτικοποίηση Dashboard", + "manage-assigned-customers": "Διαχείρηση ανατεθειμένων πελατών", + "assigned-customers": "Ανατεθειμένοι πελάτες", + "assign-to-customers": "Ανάθεση Dashboard(s) Σε Πελάτες", + "assign-to-customers-text": "Παρακαλώ επιλέξτε τους πελάτες στους οποίους θέλετε να αναθέσετε το/τα dashboard(s)", + "unassign-from-customers": "Μη Ανατεθειμένα Dashboard(s) Από Πελάτες", + "unassign-from-customers-text": "Παρακαλώ επιλέξτε τους πελάτες τους οποίους θέλετε να αφαιρέσετε από το/τα dashboard(s)", + "no-dashboards-text": "Δεν βρέθηκαν dashboards", + "no-widgets": "Δεν έχουν ρυθμιστεί widgets", + "add-widget": "Προσθέστε νέο widget", + "title": "Τίτλος", + "select-widget-title": "Επιλογή widget", + "select-widget-subtitle": "Λίστα με τους διαθέσιμους τύπους widget", + "delete": "Διαγραφή dashboard", + "title-required": "Απαιτείται ένας τίτλος.", + "description": "Περιγραφή", + "details": "Λεπτομέρειες", + "dashboard-details": "Λεπτομέρειες Dashboard", + "add-dashboard-text": "Προσθέστε νέο dashboard", + "assign-dashboards": "Αναθέστε dashboards", + "assign-new-dashboard": "Ανάθεση νέου dashboard", + "assign-dashboards-text": "Ανάθεση { count, plural, 1 {1 dashboard} other {# dashboards} } σε πελάτες", + "unassign-dashboards-action-text": "Μη ανατεθειμένο/α { count, plural, 1 {1 dashboard} other {# dashboards} } από πελάτες", + "delete-dashboards": "Διαγραφή dashboards", + "unassign-dashboards": "Αφαίρεση dashboards", + "unassign-dashboards-action-title": "Αφαίρεση { count, plural, 1 {1 dashboard} other {# dashboards} } από πελάτη", + "delete-dashboard-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το dashboard '{{dashboardTitle}}'?", + "delete-dashboard-text": "Προσοχή, μετά την επιβεβαίωση, το dashboard και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-dashboards-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "delete-dashboards-action-title": "Διααγραφή { count, plural, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text": "Προσοχή, μετά την επιβεβαίωση, όλα τα επιλεγμένα dashboards και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "unassign-dashboard-title": "Είστε σίγουρου ότι θέλετε να αφαιρέσετε το dashboard '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα αφαιρεθεί και δεν θα είναι διαθέσιμο στον πελάτη.", + "unassign-dashboard": "Αφαίρεση dashboard", + "unassign-dashboards-title": "Είστε σίγουρου ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-text": "Μετά την επιβεβαίωση όλα τα επιλεγμένα dashboards θα αφαιρεθούν και δεν θα είναι διαθέσιμα στον πελάτη.", + "public-dashboard-title": "Το dashboard είναι δημόσιο", + "public-dashboard-text": "Το dashboard {{dashboardTitle}} είναι δημόσιο και διαθέσιμο μέσω του παρακάτω συνδέσμου:", + "public-dashboard-notice": "Σημείωση: Μην ξεχνάτε να κάνετε δημόσιες τις συσκευές που συσχετίζονται ώστε να είναι δυνατή η πρόσβαση στα δεδομένα τους.", + "public-dashboard-link": "Σύνδεσμος δημόσιου dashboard", + "public-dashboard-link-text": "Το δημόσιο dashboard {{dashboardTitle}} είναι πλέον διαθέσιμο μέσω μέσω του παρακάτω συνδέσμου:", + "public-dashboard-link-notice": "Σημείωση: Μην ξεχνάτε να κάνετε δημόσιες τις συσκευές, τα assets και την προβολή οντοτήτων που συσχετίζονται ώστε να είναι δυνατή η πρόσβαση στα δεδομένα τους.", + "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό", + "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.", + "make-private-dashboard": "Κάνε το dashboard ιδιωτικό", + "socialshare-text": "'{{dashboardTitle}}' powered by EyeTech", + "socialshare-title": "'{{dashboardTitle}}' powered by EyeTech", + "select-dashboard": "Επιλογή dashboard", + "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.", + "dashboard-required": "Απαιτείται dashboard.", + "select-existing": "Επιλέξτε ένα υπάρχον dashboard", + "create-new": "Δημιουργία νέου dashboard", + "new-dashboard-title": "Τίτλος νέου dashboard", + "open-dashboard": "Άνοιγμα dashboard", + "set-background": "Ορίστε φόντο", + "background-color": "χρώμα φόντου", + "background-image": "εικόνα φόντου", + "background-size-mode": "μέγεθος φόντου", + "no-image": "Δεν επιλέχθηκε εικόνα", + "drop-image": "Ρίξτε εδώ μια εικόνα ή κάνετε κλικ για να επιλέξετε ένα αρχείο για να ανεβεί.", + "settings": "Ρυθμίσεις", + "columns-count": "Αρίθμιση στηλών", + "columns-count-required": "Απαιτείται η αρίθμιση στηλών.", + "min-columns-count-message": "Ελάχιστος αριθμός στηλών είναι 10.", + "max-columns-count-message": "Μέγιστος αριθμός στηλών είναι 1000.", + "widgets-margins": "Περιθώριο μεταξύ widgets", + "horizontal-margin": "Οριζόντιο περιθώριο", + "horizontal-margin-required": "Απαιτείται τιμή για το οριζόντιο περιθώριο.", + "min-horizontal-margin-message": "Ελάχιστη τιμή οριζόντιου περιθωρίου είναι 0.", + "max-horizontal-margin-message": "Μέγιστη τιμή οριζόντιου περιθωρίου είναι 50.", + "vertical-margin": "Κάθετο περιθώριο", + "vertical-margin-required": "Απαιτείται τιμή για το κάθετο περιθώριο", + "min-vertical-margin-message": "Ελάχιστη τιμή καθέτου περιθωρίου είναι 0.", + "max-vertical-margin-message": "Μέγιστη τιμή καθέτου περιθωρίου είναι 50.", + "autofill-height": "Αυτόματη συμπλήρωση ύψους διάταξης", + "mobile-layout": "Ρυθμίσεις διάταξης mobile", + "mobile-row-height": "ύψος γραμμής mobile, σε px", + "mobile-row-height-required": "Απαιτείται ύψος γραμμής mobile.", + "min-mobile-row-height-message": "Ελάχιστη τιμή ύψους γραμμής mobile είναι 5.", + "max-mobile-row-height-message": "Μέγιστη τιμή ύψους γραμμής mobile είναι 200.", + "display-title": "Τίτλος εμφάνισης dashboard", + "toolbar-always-open": "Γραμμή εργαλείων πάντα ανοιχτή", + "title-color": "Χρώμα τίτλου", + "display-dashboards-selection": "Εμφάνηση επιλογής dashboards", + "display-entities-selection": "Εμφάνιση επιλογής οντοτήτων", + "display-dashboard-timewindow": "Εμφάνιση χρονικού πλαισίου", + "display-dashboard-export": "Εμφάνιση εξαγωγής", + "import": "Εισαγωγή dashboard", + "export": "Εξαγωγή dashboard", + "export-failed-error": "Δεν ήταν δυνατή η εξαγωγή dashboard: {{error}}", + "export-pdf": "Εξαγωγή ως PDF", + "export-png": "Εξαγωγή ως PNG", + "export-jpg": "Εξαγωγή ως JPEG", + "export-json-config": "Εξαγωγή ρυθμίσεις JSON", + "download-dashboard-progress": "Δημιουργία dashboard {{reportType}} ...", + "create-new-dashboard": "Δημιουργία νέου dashboard", + "dashboard-file": "Αρχείο dashboard", + "invalid-dashboard-file-error": "Δεν ήταν δυνατή η εισαγωγή dashboard: Μη έγκυρη δομή δεδομένων dashboard.", + "dashboard-import-missing-aliases-title": "Διαμορφώστε τα ψευδώνυμα που χρησιμοποιούνται από το dashboard που εισαγάγατε", + "create-new-widget": "Δημιουργία νέου widget", + "import-widget": "Εισαγωγή widget", + "widget-file": "Αρχείο widget", + "invalid-widget-file-error": "Δεν ήταν δυνατή η εισαγωγή widget: Μη έγκυρη δομή δεδομένων dashboard.", + "widget-import-missing-aliases-title": "Διαμορφώστε τα ψευδώνυμα που χρησιμοποιούνται από το widget που εισαγάγατε", + "open-toolbar": "Άνοιγμα γραμμής εργαλείων dashboard", + "close-toolbar": "Κλείσιμο γραμμής εργαλείων", + "configuration-error": "Σφάλμα ρυθμίσεων", + "alias-resolution-error-title": "Σφάλμα ρυθμίσεων ψευδωνύμων dashboard", + "invalid-aliases-config": "Δεν βρέθηκε καμία συσκευή που να ταιριάζει με κάποιο από τα φίλτρα.
Παρακαλούμε επικοινωνήστε με τον Διαχειριστή για την επίλυση του ζητήματος.", + "select-devices": "Επιλογή συσκευών", + "assignedToCustomer": "Ανατεθειμένο σε πελάτη", + "assignedToCustomers": "Ανατεθειμένο σε πελάτες", + "public": "Δημόσιο", + "public-link": "Δημόσιος σύνδεσμος", + "copy-public-link": "Αντιγραφή δημόσιου συνδέσμου", + "public-link-copied-message": "Ο δημόσιος σύνδεσμος έχει αντιγραφεί στο πρόχειρο", + "manage-states": "Διαχείρηση κατάστασης dashboard", + "states": "Κατάσταση dashboard", + "search-states": "Αναζήτηση σε κατάσταση dashboard", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } επιλεγμένα", + "edit-state": "Επεξεργασία κατάστασης dashboard", + "delete-state": "Διαγραφή κατάστασης dashboard", + "add-state": "Προσθήκη κατάστασης dashboard", + "state": "Κατάσταση Dashboard", + "state-name": "Όνομα", + "state-name-required": "Απαιτείται όνομα κατάστασης dashboard.", + "state-id": "ID Κατάστασης", + "state-id-required": "Απαιτείται ID κατάστασης dashboard.", + "state-id-exists": "Υπάρχει ήδη κατάσταση dashboard με το ίδιο ID.", + "is-root-state": "Root state", + "delete-state-title": "Διαγραφή κατάστασης dashboard", + "delete-state-text": "Είστε σίγουροι ότι θέλετε να διαγράψετε την κατάσταση dashboard με όνομα '{{stateName}}'?", + "show-details": "Εμφάνιση λεπτομερειών", + "hide-details": "Απόκρυψη λεπτομερειών", + "select-state": "Επιλογή κατάστασης", + "state-controller": "Έλεγχος κατάστασης", + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } επιλεγμένα", + "search": "Αναζήτηση dashboards", + "select-group-to-add": "Επιλογή ομάδας για να προστεθεί στα επιλεγμένα dashboards", + "select-group-to-move": "Επιλογή ομάδας για να μετακινηθεί στα επιλεγμένα dashboards", + "remove-dashboards-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 dashboard} other {# dashboards} } from group '{entityGroup}'?", + "group": "Ομάδα dashboards", + "list-of-groups": "{ count, plural, 1 {One dashboard group} other {List of # dashboard groups} }", + "group-name-starts-with": "Ομάδες dashboard των οποίων το όνομα αρχίζει από '{{prefix}}'" + }, + "datakey": { + "settings": "Ρυθμίσεις", + "advanced": "Προχωρημένες", + "label": "Ετικέτα", + "color": "Χρώμα", + "units": "Ειδικό σύμβολο που εμφανίζεται δίπλα από την τιμή", + "decimals": "Αριθμός ψηφίων μετά την υποδιαστολή", + "data-generation-func": "Λειτουργία δημιουργίας δεδομένων", + "use-data-post-processing-func": "Χρησιμοποιήστε τη λειτουργία μετα-επεξεργασίας δεδομένων", + "configuration": "Ρυθμίσεις ονομάτων δεδομένων", + "timeseries": "Χρονική σειρά", + "attributes": "Ιδιώτητες", + "alarm": "Πεδία alarm", + "timeseries-required": "Απαιτείται χρονική σειρά οντοτήτων.", + "timeseries-or-attributes-required": "Απαιτείται χρονική σειρά/ιδιώτητες οντοτήτων.", + "maximum-timeseries-or-attributes": "Μέγιστο { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "Απαιτούνται πεδία alarm.", + "function-types": "Τύποι λειτουργιών", + "function-types-required": "Απαιτούνται τύποι λειτουργιών.", + "maximum-function-types": "Μέγιστο { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", + "time-description": "χρονικό σημείο της συγκεκριμένης τιμής", + "value-description": "η συγκεκριμένη τιμή", + "prev-value-description": "αποτέλεσμα της προηγούμενης λειτουργίας", + "time-prev-description": "χρονσικό σημείο της προηγούμενης τιμής", + "prev-orig-value-description": "αρχική προηγούμενη τιμή" + }, + "datasource": { + "type": "Τύπος πηγής δεδομένων", + "name": "Όνομα", + "add-datasource-prompt": "Παρακαλούμε προσθέστε πηγή δεδομένων" + }, + "details": { + "details": "Λεπτομέρειες", + "edit-mode": "Λειτουργία Επεξεργασίας", + "toggle-edit-mode": "Εναλλαγή λειτουργίας επεξεργασίας" + }, + "device": { + "device": "Συσκευή", + "device-required": "Απαιτείται ορισμός συσκευής.", + "devices": "Συσκευές", + "management": "Διαχείριση Συσκευών", + "view-devices": "Προβολή Συσκευών", + "device-alias": "Ψευδώνυμο συσκευής", + "aliases": "Ψευδώνυμα συσκευής", + "no-alias-matching": "Δεν βρέθηκε '{{alias}}'.", + "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", + "no-key-matching": "Δεν βρέθηκε '{{key}}'.", + "no-keys-found": "Δεν βρέθηκαν ονόματα", + "create-new-alias": "Δημιουργήστε ένα νέο ψευδώνυμο!", + "create-new-key": "Δημιουργήστε ένα νέο!", + "duplicate-alias-error": "Βρέθηκε διπλότυπο το ψευδώνυμο '{{alias}}'.
Τα ψευδώνυμα συσκευών πρέπει να είναι μοναδικά σε ένα dashboard.", + "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", + "no-devices-matching": "Δεν βρέθηκε συσκευή η οποία να αντιστοιχεί σε '{{entity}}'.", + "alias": "Ψευδώνυμο", + "alias-required": "Απαιτείται ψευδώνυμο συσκευής.", + "remove-alias": "Αφαίρεση ψευδώνυμου συσκευής", + "add-alias": "Προσθήκη ψευδώνυμου συσκευής", + "name-starts-with": "Το όνομα συσκευής αρχίζει με", + "device-list": "Λίστα συσκευών", + "use-device-name-filter": "Χρήση φίλτρου", + "device-list-empty": "Δεν επιλέχθηκαν συσκευές.", + "device-name-filter-required": "Απαιτείται φίλτρο ονόματος συσκυεής.", + "device-name-filter-no-device-matched": "Δεν βρέθηκαν συσκευές οι οποίες να αρχίζουν από '{{device}}'.", + "add": "Προσθήκη συσκευής", + "assign-to-customer": "Ανάθεση σε πελάτη", + "assign-device-to-customer": "Ανάθεση Συσκευών Σε Πελάτη", + "assign-device-to-customer-text": "Παρακαλούμε επιλέξτε συσκευές προς ανάθεση σε πελάτη", + "make-public": "Δημοσιοποίηση συσκευών", + "make-private": "Ιδιωτικοποίηση συσκευών", + "no-devices-text": "Δεν βρέθηκαν συσκευές", + "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να αναθέσετε τις συσκευές", + "device-details": "Λεπτομέρειες συσκευές", + "add-device-text": "Προσθήκη νέας συσκευής", + "credentials": "Διαπιστευτήρια", + "manage-credentials": "Διαχείρηση διαπιστευτηρίων", + "delete": "Διαγραφή συσκευής", + "assign-devices": "Ανάθεση συσκευών", + "assign-devices-text": "Ανάθεση { count, plural, 1 {1 device} other {# devices} } σε πελάτη", + "delete-devices": "Διαγραφή συσκευών", + "unassign-from-customer": "Αφαίρεση από πελάτη", + "unassign-devices": "Αφαίρεση συσκευών", + "unassign-devices-action-title": "Αφαίρεση { count, plural, 1 {1 device} other {# devices} } από πελάτη", + "assign-new-device": "Ανάθεση νέων συσκευών", + "make-public-device-title": "Είστε σίγουροι ότι θέλετε να κάνετε τη συσκευή '{{deviceName}}' δημόσια;", + "make-public-device-text": "Μετά από την επιβεβαίωσή σας η συσκευή και όλα τα δεδομένα της θα είναι δημόσια και διαθέσιμα σε τρίτους.", + "make-private-device-title": "Είστε σίγουροι ότι θέλετε να κάνετε τη συσκευή '{{deviceName}}' ιδιωτική;", + "make-private-device-text": "Μετά από την επιβεβαίωσή σας η συσκευή και όλα τα δεδομένα της θα είναι ιδιωτικά και δεν θα είναι διαθέσιμα σε τριτους.", + "view-credentials": "Προβολή διαπιστευτηρίων", + "delete-device-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την συσκευή '{{deviceName}}';", + "delete-device-text": "Προσοχή, μετά την επιβεβαίωση, η συσκευή και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-devices-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 device} other {# devices} };", + "delete-devices-action-title": "Διαγραφή { count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες συσκευές θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "unassign-device-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την συσκευή '{{deviceName}}'?", + "unassign-device-text": "Μετά την επιβεβαίωση, η συσκευή θα αφαιρεθεί και δεν θα είναι προσβάσιμες από τον πελάτη.", + "unassign-device": "Αφαίρεση συσκευής", + "unassign-devices-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-text": "Μετά την επιβεβαίωση, όλες οι επιλεγμένες συσκευές θα καταργηθούν και δεν θα είναι προσβάσιμες από τον πελάτη.", + "device-credentials": "Πιστοποιητικά συσκευής", + "credentials-type": "Τύπος διαπιστευτηρίων", + "access-token": "Διακριτικό πρόσβασης", + "access-token-required": "Απαιτείται διακριτικό πρόσβασης.", + "access-token-invalid": "Access token length must be from 1 to 20 characters.", + "rsa-key": "RSA public key", + "rsa-key-required": "Απαιτείται RSA public key.", + "secret": "Secret", + "secret-required": "Απαιτείται secret.", + "device-type": "Τύπος συσκευής", + "device-type-required": "Απαιτείται τύπος συσκεύης.", + "select-device-type": "Επιλογή τύπου συσκευής", + "enter-device-type": "Είσοδο τύπου συσκευής", + "any-device": "Οποιαδήποτε συσκευή", + "no-device-types-matching": "Δεν βρέθηκε συσκευή η οποία να αντιστοιχεί σε '{{entitySubtype}}'.", + "device-type-list-empty": "Δεν έχουν επιλεχθεί τύποι συσκευής.", + "device-types": "Τύποι συσκευής", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "label": "Ετικέτα", + "events": "Γεγονότα", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ID συσκευής", + "copyAccessToken": "Αντιγραφή διακριτικού διαπιστευτηρίου", + "idCopiedMessage": "Το ID της συσκευής έχει αντιγραφεί στο πρόχειρο", + "accessTokenCopiedMessage": "Το διακριτικό διαπιστευτήριο έχει αντιγραφεί στο πρόχειρο", + "assignedToCustomer": "Ανάθεση σε πελάτη", + "unable-delete-device-alias-title": "Αδύνατο να διαγραφεί το ψευδώνυμο συσκευής", + "unable-delete-device-alias-text": "Το ψευδώνυμο συσκευής '{{deviceAlias}}' δεν μπορεί να διαγραφεί όσο εξακωλουθεί να χρησιμοποιείται από τα ακόλουθα widget(s):
{{widgetsList}}", + "is-gateway": "είναι gateway", + "public": "Δημόσιο", + "device-public": "Η συσκευή είναι δημόσια", + "select-device": "Επιλογή συσκευής", + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } επιλέχθηκαν", + "search": "Αναζήτηση συσκευών", + "select-group-to-add": "Επιλέξτε την ομάδα στην οποία θα προστεθουν οι επιλεγμένες συσκευές", + "select-group-to-move": "Επιλέξτε την ομάδα στην οποία θα μετακινηθούν οι επιλεγμένες συσκευές", + "remove-devices-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 device} other {# devices} } from group '{entityGroup}'?", + "group": "Ομάδα Συσκευών", + "list-of-groups": "{ count, plural, 1 {One device group} other {List of # device groups} }", + "group-name-starts-with": "Ομάδες συσκευών των οποίων το όνομα αρχίζει από '{{prefix}}'", + "import": "Εισαγωγή συσκευής", + "device-file": "Αρχείο συσκευής" + }, + "dialog": { + "close": "Κλείσιμο διαλόγου" + }, + "direction": { + "column": "Στήλη", + "row": "Γραμμή" + }, + "error": { + "unable-to-connect": "Αδύνατον να συνδεθεί στον διακομιστή! Παρακαλούμε ελέγξτε την σύνδεσή σας στο ίντερνετ.", + "unhandled-error-code": "Μη διορθωμένος κωδικός σφάλματος: {{errorCode}}", + "unknown-error": "Άγνωστο σφάλμα" + }, + "entity": { + "entity": "Οντότητα", + "entities": "Οντότητες", + "aliases": "Ψευδώνυμα οντότητας", + "entity-alias": "Ψευδώνυμο οντότητας", + "unable-delete-entity-alias-title": "Αδύνατο να διαγραφεί το ψευδώνυμο οντότητας", + "unable-delete-entity-alias-text": "Το ψευδώνυμο οντότητας '{{entityAlias}}' δεν μπορεί να διαγραφεί όσο εξακολουθεί να χρησιμοποιείται από τα παρακάτω widget(s):
{{widgetsList}}", + "duplicate-alias-error": "Βρέθηκε διπλότυπο το ψευδώνυμο '{{alias}}'.
Τα ψευδώνυμα οντοτήτων πρέπει να είναι μοναδικά σε ένα dashboard.", + "missing-entity-filter-error": "Λείπει φίλτρο από το ψευδώνυμο '{{alias}}'.", + "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", + "alias": "Ψευδώνυμο", + "alias-required": "Απαιτείται ψευδώνυμο οντότητας.", + "remove-alias": "Αφαίρεση ψευδώνυμου οντότητας", + "add-alias": "Προσθήκη ψευδώνυμου οντότητας", + "entity-list": "Λίστα οντοτήτων", + "entity-type": "Τύπος οντοτήτων", + "entity-types": "Τύποι οντοτήτων", + "entity-type-list": "Λίστα τύπων οντοτήτων", + "any-entity": "Οποιαδήποτε οντότητα", + "enter-entity-type": "Εισαγωγή τύπου οντότητας", + "no-entities-matching": "Δεν βρέθηκαν οντότητες ο οποίες να σχετίζονται με '{{entity}}'.", + "no-entity-types-matching": "Δεν βρέθηκαν τύποι οντότητας ο οποίοι να σχετίζονται με '{{entityType}}'.", + "name-starts-with": "το όνομα αρχίζει από", + "use-entity-name-filter": "χρήση φίλτρου", + "entity-list-empty": "Δεν έχουν επιλεγεί οντότητες.", + "entity-type-list-empty": "Δεν έχουν επιλεγεί τύποι οντότητας.", + "entity-name-filter-required": "Απαιτείται φίλτρο ονόματος οντότητας.", + "entity-name-filter-no-entity-matched": "Δεν βρέθηκαν οντότητες οι οποίες αρχίζουν από '{{entity}}'.", + "all-subtypes": "Όλοι", + "select-entities": "Επιλογή οντοτήτων", + "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", + "no-alias-matching": "Δεν βρέθηκε '{{alias}}'.", + "create-new-alias": "Δημιουργείστε ένα νέο!", + "key": "Κλειδί", + "key-name": "όνομα κλειδιού", + "no-keys-found": "δεν βρέθηκαν κλειδιά.", + "no-key-matching": "Δεν βρέθηκε '{{key}}'.", + "create-new-key": "Δημιουργείστε ένα νέο!", + "type": "Τύπος", + "type-required": "Απαιτείται τύπος οντότητας.", + "type-device": "Συσκευή", + "type-devices": "Συσκευές", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "Συσκευές των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-asset": "Asset", + "type-assets": "Assets", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "Assets των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-entity-view": "Προβολή οντότητας", + "type-entity-views": "Προβολές οντότητας", + "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }", + "entity-view-name-starts-with": "Προβολές οντότητας των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-rule": "Κανόνας", + "type-rules": "Κανόνες", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "Κανόνες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-plugin": "Πρόσθετο", + "type-plugins": "Προσθετα", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "Πρόσθετα των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-tenant": "Μισθωτής", + "type-tenants": "Μισθωτές", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "Μισθωτές των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-customer": "Πελάτης", + "type-customers": "Πελάτες", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "Πελάτες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-user": "Χρήστης", + "type-users": "Χρήστες", + "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", + "user-name-starts-with": "Χρήστες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-dashboard": "Dashboard", + "type-dashboards": "Dashboards", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "Dashboards των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-alarm": "Alarm", + "type-alarms": "Alarms", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "Alarms των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-rulechain": "Αλυσίδα Κανόνων", + "type-rulechains": "Αλυσίδες Κανόνων", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", + "rulechain-name-starts-with": "Αλυσίδες Κανόνων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-scheduler-event": "Προγραμματιστής", + "type-scheduler-events": "Προγραμματιστής", + "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", + "scheduler-event-name-starts-with": "Προγραμματιστές γεγονότων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-blob-entity": "Ογκώδη οντότητα", + "type-blob-entities": "Ογκώδεις οντότητες", + "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", + "blob-entity-name-starts-with": "Ογκώδεις οντότητες των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-rulenode": "Κόμβος κανόνα", + "type-rulenodes": "Κόμβοι κανόνων", + "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", + "rulenode-name-starts-with": "Κόμβοι κανόνων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-current-customer": "Τρέχον Πελάτης", + "search": "Αναζήτηση Οντότητες", + "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } επιλεγμένα", + "entity-name": "Όνομα οντότητας", + "details": "Λεπτομέρειες οντότητας", + "no-entities-prompt": "Δεν βρέθηκαν οντότητες", + "no-data": "Δεν υπάρχουν δεδομένα προς προβολή", + "columns-to-display": "στήλες που προβάλονται", + "type-entity-group": "Ομάδα Οντότητας", + "type-converter": "Μετατροπέας Δεδομένων", + "type-converters": "Μετατροπείς Δεδομένων", + "list-of-converters": "{ count, plural, 1 {One data converter} other {List of # data converters} }", + "converter-name-starts-with": "Μετατροπείς δεδομένων των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-integration": "Ενσωμάτωση", + "type-integrations": "Ενσωματώσεις", + "list-of-integrations": "{ count, plural, 1 {One integration} other {List of # integrations} }", + "integration-name-starts-with": "Ενσωματώσεις των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-role": "Ρόλος", + "type-roles": "Ρόλοι", + "list-of-roles": "{ count, plural, 1 {One role} other {List of # roles} }", + "role-name-starts-with": "Ρόλοι των οποίων το όνομα αρχίζει από '{{prefix}}'", + "type-group-permission": "Άδεια Ομάδας" + }, + "entity-group": { + "entity-group": "Ομάδα Οντότητας", + "details": "Λεπτομέρειες", + "columns": "Στήλες", + "add-column": "Προσθήκη στήλης", + "column-value": "Τιμή", + "column-value-required": "Απαιτείται τιμή στήλης.", + "column-title": "Τίτλος", + "default-sort-order": "Προεπιλογή ταξινόμισης", + "default-sort-order-required": "Απαιτείται προεπιλογή ταξινόμισης στήλης.", + "hide-in-mobile-view": "κρυφό σε mobile", + "use-cell-style-function": "Λειτουργία χρήσης στυλ κελιού", + "use-cell-content-function": "Λειτουργία χρήσης περιεχομένου κελιού", + "edit-column": "Επεξεργασία στήλης", + "column-details": "Λεπτομέρειες στήλης", + "actions": "Ενέργιες", + "settings": "Ρυθμίσεις", + "delete": "Διαγραφή ομάδας οντοτήτων", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "add": "Προσθήκη ομάδας οντοτήτων", + "add-entity-group-text": "Προσθήκη νέας ομάδας οντοτήτων", + "no-entity-groups-text": "Δεν βρέθηκαν ομάδες οντοτήτων", + "entity-group-details": "Λεπτομέρειες ομαδών οντοτήτων", + "delete-entity-groups": "Διαγραφή ομαδών οντοτήτων", + "delete-entity-group-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την ομάδα οντοτήτων '{{entityGroupName}}'?", + "delete-entity-group-text": "Προσοχή, μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-entity-groups-title": "Είστε σίγουροι ότι θέλετε να διαγραφούν { count, plural, 1 {1 entity group} other {# entity groups} }?", + "delete-entity-groups-action-title": "Διαγραφή { count, plural, 1 {1 entity group} other {# entity groups} }", + "delete-entity-groups-text": "Προσοχή, αφού ολοκληρωθεί η επιβεβαίωση, όλες οι επιλεγμένες ομάδες οντοτήτων θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "device-groups": "Ομάδες Συσκευών", + "asset-groups": "Ομάδες Asset", + "customer-groups": "Ομάδες Πελατών", + "device-group": "Ομάδα Πελατών", + "asset-group": "Ομάδα Asset", + "user-group": "Ομάδα Χρηστών", + "user-groups": "Ομάδες Χρηστών", + "customer-group": "Ομάδα Χρηστών", + "entity-view-groups": "Ομάδες Όψεων", + "entity-view-group": "Προβολή ομαδών οντοτήτων", + "dashboard-groups": "Ομάδες Dashboard", + "dashboard-group": "Ομάδα Dashboard", + "fetch-more": "Περισσότερα", + "column-type": { + "column-type": "Τύπος στήλης", + "client-attribute": "Χαρακτηριστικά Client", + "shared-attribute": "Κοινόχρηστα Χαρακτηριστικά", + "server-attribute": "Χαρακτηριστικό Server", + "timeseries": "Χρονική σειρά", + "entity-field": "Πεδίο οντότητας" + }, + "column-type-required": "Απαιτείται τύπος στήλης.", + "entity-field": { + "created-time": "Δημιουργήθηκε", + "name": "Όνομα", + "type": "Τύπος", + "assigned_customer": "Ανατεθειμένος πελάτης", + "authority": "Εξουσιοδότηση", + "first_name": "Όνομα", + "last_name": "Επίθετο", + "email": "Email", + "title": "Τίτλος", + "country": "Χώρα", + "state": "Νομός", + "city": "Πόλη", + "address": "Διεύθυνση", + "address2": "Διεύθυνση 2", + "zip": "Τ.Κ.", + "phone": "Τηλέφωνο" + }, + "sort-order": { + "asc": "Αύξουσα", + "desc": "Φθίνουσα", + "none": "Καμία" + }, + "details-mode": { + "on-row-click": "στο κλικ στη γραμμη", + "on-action-button-click": "στο κλικ στο κουμπί λεπτομέρειες", + "disabled": "μη διαθέσιμο" + }, + "change-owner": "Αλλαγή ιδιοκτήτη", + "select-target-owner": "Επιλογή ιδιοκτήτη", + "no-owners-matching": "Δεν βρέθηκε ιδιοκτήτης που να αντιστοιχεί σε '{{owner}}'.", + "target-owner-required": "Απαιτείται επιλεγμένος ιδιοκτήτης.", + "confirm-change-owner-title": "Είστε σίγουροι ότι θέλετε να αλλάξετε ιδιοκτήτη για { count, plural, 1 {1 selected entity} other {# selected entities} }?", + "confirm-change-owner-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες οντότητες θα αφαιρεθούν από τον τρέχοντα ιδιοκτήτη και θα τοποθετηθούν στην ομάδα 'Όλα' του επιλεγμένου ιδιοκτήτη.", + "add-to-group": "Προσθήκη σε ομάδα", + "move-to-group": "Μετακίνηση σε ομάδα", + "select-entity-group": "Επιλογή ομάδας οντοτήτων", + "no-entity-groups-matching": "Δεν βρέθηκαν ομάδες οντοτήτων που να αντιστοιχούν με '{{entityGroup}}'.", + "target-entity-group-required": "Απαιτείται επιλεγμένη ομάδα οντοτήτων.", + "select-user-group": "Επιλογή ομάδας χρηστών", + "no-user-groups-matching": "Δεν βρέθηκαν ομάδες χρηστών οι οποίες να αντιστοιχούν σε '{{entityGroup}}'.", + "target-user-group-required": "Απαιτείται επιλεγμένη ομάδα χρηστών.", + "remove-from-group": "Αφαίρεση από ομάδα", + "group-table-title": "Τίτλος ομάδας πίνακα", + "enable-search": "Ενεργοποίηση αναζήτησης οντοτήτων", + "enable-add": "Ενεργοποίηση προσθήκης οντοτήτων", + "enable-delete": "Ενεργοποίηση διαγραφής οντοτήτων", + "enable-selection": "Ενεργοποίηση επιλογής οντοτήτων", + "enable-group-transfer": "Ενεργοποίηση μεταφοράς ομάδων", + "display-pagination": "Προβολή σελιδοποίησης", + "default-page-size": "Προεπιλεγμένο μέγεθος σελίδας", + "enable-assignment-actions": "Ενεργοποίηση ανάθεσης", + "enable-credentials-management": "Ενεργοποίηση διαχείρησης διαπιστευτηρίων", + "enable-login-as-user": "Ενεργοποίηση εισόδου ως χρήστη", + "enable-users-management": "Ενεργοποίηση διαχείρισης χρήστών", + "enable-customers-management": "Ενεργοποίηση διαχείρησης πελατών", + "enable-assets-management": "Ενεργοποίηση διαχείρησης assets", + "enable-devices-management": "Ενεργοποίηση διαχείρησης συσκευών", + "enable-entity-views-management": "Ενεργοποίηση διαχείρησης προβολής οντοτήτων", + "enable-dashboards-management": "Ενεργοποίηση διαχείρησης dashboard", + "open-details-on": "Άνοιγμα λεπτομερειών οντότητας σε", + "select-existing": "Ε[ιλογή υπάρχον ομάδας οντοτήτων", + "create-new": "Δημιουργία νέας ομάδας οντοτήτων", + "new-entity-group-name": "Νέο όνομα ομάδας οντοτήτων", + "entity-group-list": "Λίστα ομάδων οντοτήτων", + "entity-group-list-empty": "Δεν έχουν επιλεχθεί ομάδες οντοτήτων.", + "name-starts-with": "Το όνομα της ομάδας οντοτήτων αρχίζει από", + "entity-group-name-filter-required": "Απαιτείται φίλτρο ονόματος ομάδας οντοτήτων.", + "roles": "Ρόλοι", + "permissions": "Δικαιώματα", + "public": "Δημόσιο", + "entity-group-public": "Η ομάδα οντοτήτων είναι δημόσια", + "make-public": "Δημοσιοποίηση ομάδας οντοτήτων", + "make-private": "Ιδιωτικοποίηση ομάδας οντοτήτων", + "make-public-entity-group-title": "Είστε σίγουροι ότι θέλετε να κάνετε την ομάδα οντοτήτων '{{entityGroupName}}' δημόσια;", + "make-public-entity-group-text": "Μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλες οι οντότητές της θα δημοσιοποιηθούν και θα είναι προσβάσιμες από τρίτους.", + "make-private-entity-group-title": "Είστε σίγουροι ότι θέλετε να κάνετε την ομάδα οντοτήτων '{{entityGroupName}}' ιδιωτική?", + "make-private-entity-group-text": "Μετά την επιβεβαίωση, η ομάδα οντοτήτων και όλες οι οντότητές της θα γίνουν ιδιωτικές και δεν θα είναι προσβάσιμες από τρίτους.", + "copyId": "Αντιγραφή ID ομάδας οντοτήτων", + "idCopiedMessage": "Το ID της ομάδας οντοτήτων έχει αντιγραφεί στο πρόχειρο" + }, + "entity-view": { + "entity-view": "Όψη Οντότητας", + "entity-view-required": "Απαιτείται προβολή οντότητας.", + "entity-views": "Όψεις Οντοτήτων", + "management": "Διαχείριση Όψεων Οντοτήτων", + "view-entity-views": "Προβολή οντοτήτων", + "entity-view-alias": "Ψευδώνυμο προβολής οντότητας", + "aliases": "Ψευδώνυμα προβολής οντότητας", + "no-alias-matching": "'Δεν βρέθηκαν {{alias}}'.", + "no-aliases-found": "Δεν βρέθηκαν ψευδώνυμα.", + "no-key-matching": "'Δεν βρέθηκαν {{key}}'.", + "no-keys-found": "Δεν βρέθηκαν ονόματα.", + "create-new-alias": "Δημιουργήστε ένα νέο!", + "create-new-key": "Δημιουργήστε ένα νέο!", + "duplicate-alias-error": "Βρέθηκε διπλότυπο '{{alias}}'.
Τα ψευδώνυμα προβολής οντότητας πρέπει να είναι μοναδικά εντός του dashboard.", + "configure-alias": "Ρύθμιση ψευδώνυμου '{{alias}}'", + "no-entity-views-matching": "Δεν βρέθηκαν προβολές οντοτήτων οι οποίες να αντιστοιχούν σε '{{entity}}'.", + "alias": "Ψευδώνυμο", + "alias-required": "Απαιτείται ψευδώνυμο προβολής οντοτήτων.", + "remove-alias": "Αφαίρεση ψευδωνύμου προβολής οντοτήτων", + "add-alias": "Προβολή ψευδωνύμου προβολής οντοτήτων", + "name-starts-with": "Το όνομα προβολής οντότητας αρχίζει από", + "entity-view-list": "Λίστα προβολής οντότητας", + "use-entity-view-name-filter": "Χρήση φίλτρου", + "entity-view-list-empty": "Δεν έχουν επιλεχθεί προβολές οντότητας.", + "entity-view-name-filter-required": "Απαιτείται φίλτρο ονόματος προβολής οντότητας.", + "entity-view-name-filter-no-entity-view-matched": "Δεν βρέθηκαν προβολές οντότητας οι οποίες να αρχίζουν από '{{entityView}}'.", + "add": "Προσθήκη Προβολής Οντότητας", + "assign-to-customer": "Ανάθεση σε πελάτη", + "assign-entity-view-to-customer": "Ανάθεση προβολής/ών οντότητας σε πελάτη", + "assign-entity-view-to-customer-text": "Παρακαλώ επιλέξτε τις προβολές οντότητας για να αναθέσετε σε πελάτη", + "no-entity-views-text": "Δεν βρέθηκαν προβολές οντότητας", + "assign-to-customer-text": "Παρακαλώ επιλέξτε πελάτη για να ανάθεση προβολών οντότητας", + "entity-view-details": "Λεπτομέρειες προβολής οντότητας", + "add-entity-view-text": "Προσθήκη νέας προβολής οντότητας", + "delete": "Διαγραφή προβολής οντότητας", + "assign-entity-views": "Ανάθεση προβολών οντότητας", + "assign-entity-views-text": "Ανάθεση { count, plural, 1 {1 entityView} other {# entityViews} } σε πελάτη", + "delete-entity-views": "Διαγραφή προβολών οντότητας", + "make-public": "Δημοσιοποίηση προβολής οντότητας", + "make-private": "Ιδιωτικοποίηση προβολής οντότητας", + "unassign-from-customer": "Αφαίρεση από πελάτη", + "unassign-entity-views": "Αφαίρεση προβολών οντότητας", + "unassign-entity-views-action-title": "Αφαίρεση { count, plural, 1 {1 entityView} other {# entityViews} } από πελάτη", + "assign-new-entity-view": "Ανάθεση νέας προβολής οντότητας", + "delete-entity-view-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την προβολή οντότητας '{{entityViewName}}'?", + "delete-entity-view-text": "Προσοχή, μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-entity-views-title": "Είστε σίγουροι ότι θέλετε να προβάλετε την οντότητα { count, plural, 1 {1 entityView} other {# entityViews} };", + "delete-entity-views-action-title": "Διαγραφή { count, plural, 1 {1 entityView} other {# entityViews} }", + "delete-entity-views-text": "Προσοχή, μετά την επιβεβαίωση, όλες οι επιλεγμένες προβολές της οντότητας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "make-public-entity-view-title": "Είστε σίγουροι ότι θέλετε να κάνετε την προβολή οντότητας '{{entityViewName}}' δημόσια?", + "make-public-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα δεδομένα της θα δημοσιοποιηθούν και θα είναι προσβάσιμα από τρίτους.", + "make-private-entity-view-title": "Είστε σίγουροι ότι θέλετε να κάνετε την προβολή οντότητας '{{entityViewName}}' ιδιωτική;", + "make-private-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας και όλα τα δεδομένα της θα γίνουν ιδιωτικά και θα δεν είναι προσβάσιμα από τρίτους.", + "unassign-entity-view-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την προβολη οντότητας '{{entityViewName}}';", + "unassign-entity-view-text": "Μετά την επιβεβαίωση, η προβολή της οντότητας θα καταργηθεί και δεν θα είναι προσβάσιμη από τον πελάτη.", + "unassign-entity-view": "Αφαίρεση προβολής οντότητας", + "unassign-entity-views-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 entityView} other {# entityViews} };", + "unassign-entity-views-text": "Μετά την επιβεβαίωση, όλες οι επιλεγμένες προβολές οντοτήτων θα αφαιρεθούν και δεν θα είναι προσβάσιμες από τον πελάτη.", + "entity-view-type": "Τύπος Προβολής Οντότητας", + "entity-view-type-required": "Απαιτείται τύπος προβολής οντότητας.", + "select-entity-view-type": "Επιλογή τύπου προβολής οντότητας", + "enter-entity-view-type": "Εισαγωγή τύπου προβολής οντοτήτων", + "any-entity-view": "Οποιαδήποτε προβολή οντότητας", + "no-entity-view-types-matching": "Δεν βρέθηκαν τύποι προβολής οντότητας οι οποίοι να αντιστοιχούν με '{{entitySubtype}}'.", + "entity-view-type-list-empty": "Δεν έχουν επιλεχθεί τύποι προβολής οντότητας.", + "entity-view-types": "Τύποι Προβολής Οντότητας", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "events": "Γεγονότα", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ID προβολής οντότητας", + "idCopiedMessage": "Το ID της προβολής οντότητας έχει αντιγραφεί στο πρόχειρο", + "assignedToCustomer": "Αναθέση σε πελάτη", + "unable-entity-view-device-alias-title": "Αδύνατον να διαγραφεί το ψευδώνυμο προβολής οντότητας", + "unable-entity-view-device-alias-text": "Το ψευδώνυμο συσκευής '{{entityViewAlias}}' δεν μπορεί να διαγραφεί όσο εξακωλουθεί να χρησιμοποιείτε από τα παρακάτω widget(s):
{{widgetsList}}", + "select-entity-view": "Επιλογή προβολής οντότητας", + "start-date": "Ημερομηνία έναρξης", + "start-ts": "Ώρα έναρξης", + "end-date": "Ημερομηνία λήξης", + "end-ts": "Ώρα λήξης", + "date-limits": "Όρια ημερομηνίας", + "client-attributes": "Χαρακτηριστικά Client", + "shared-attributes": "Κοινόχρηστα Χαρακτηριστικά", + "server-attributes": "Χαρακτηριστικά Server", + "timeseries": "Χρονική σειρά", + "client-attributes-placeholder": "Χαρακτηριστικά Client", + "shared-attributes-placeholder": "Κοινόχρηστα Χαρακτηριστικά", + "server-attributes-placeholder": "Χαρακτηριστικά Server", + "timeseries-placeholder": "Χρονική σειρά", + "target-entity": "Στοχευμένη οντότητα", + "attributes-propagation": "Διάδοση χαρακτηριστικών", + "attributes-propagation-hint": "Η προβολή οντοτήτων θα αντιγράφει αυτόματα καθορισμένα χαρακτηριστικά από την στοχευμένη οντότητα κάθε φορά που αποθηκεύετε ή ενημερώνετε αυτήν την προβολή οντότητας. Για λόγους απόδοσης, τα χαρακτηριστικά της στοχευμένης οντότητας δεν μεταδίδονται στην προβολή οντότητας με κάθε αλλαγή χαρακτηριστικών. Μπορείτε να ενεργοποιήσετε την αυτόματη διάδοση ρυθμίζοντας τον κόμβο \"αντιγραφή για προβολή \" στην αλυσίδα κανόνων σας και συνδέοντας τα μηνύματα \"Χαρακτηριστικά Post \" και \"Ενημερωμένα Χαρακτηριστικά \" στον νέο κόμβο.", + "timeseries-data": "Δεδομένα χρονικής σειράς", + "timeseries-data-hint": "Ρυθμίστε τα δεδομένα της χρονικής σειράς της στοχευμένης οντότητας που θα είναι διαθέσιμα στην προβολή οντοτητας. Αυτά τα δεδομένα χρονικής σειράς είναι μόνο για ανάγνωση.", + "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } επιλεγμένα", + "search": "Αναζήτηση προβολών οντότητας", + "select-group-to-add": "Επιλογή ομάδας για προσθήκη των επιλεγμένων προβολών οντότητας", + "select-group-to-move": "Επιλογή ομάδας για μετακίνηση των επιλεγμένων προβολών οντότητας", + "remove-entity-views-from-group": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε { count, plural, 1 {1 entity view} other {# entity views} } from group '{entityGroup}'?", + "group": "Ομάδα προβολών οντότητας", + "list-of-groups": "{ count, plural, 1 {One entity view group} other {List of # entity view groups} }", + "group-name-starts-with": "Ομάδες προβολής οντότητας των οποίων το όνομα αρχίζει από '{{prefix}}'" + }, + "event": { + "events": "Γεγονότα", + "event-type": "Τύπος Γεγονότος", + "type-error": "Σφάλμα", + "type-lc-event": "Γεγονός κύκλου ζωής", + "type-stats": "Στατιστική", + "type-debug-converter": "Αποσφαλμάτωση", + "type-debug-integration": "Αποσφαλμάτωση", + "type-debug-rule-node": "Αποσφαλμάτωση", + "type-debug-rule-chain": "Αποσφαλμάτωση", + "no-events-prompt": "Δεν βρέθηκαν γεγονότα", + "error": "Σφάλμα", + "alarm": "Alarm", + "event-time": "Ώρα Γεγονότος", + "server": "Διακομιστής", + "body": "Σώμα (body)", + "method": "Μέθοδος", + "type": "Τύπος", + "in": "Είσοδος", + "out": "Έξοδος", + "metadata": "Μεταδεδομένα", + "message": "Μήνυμα", + "entity": "Οντότητα", + "message-id": "ID Μηνύματος", + "message-type": "Τύπος Μηνύματος", + "data-type": "Τύπος Δεδομένων", + "relation-type": "Τύπος Σχέσης", + "data": "Δεδομέναα", + "event": "Γεγονός", + "status": "Κατάσταση", + "success": "Επιτυχία", + "failed": "Απέτυχε", + "messages-processed": "Επεξεργασμένα μηνύματα", + "errors-occurred": "Παρουσιάστηκαν σφάλματα" + }, + "extension": { + "extensions": "Επεκτάσεις", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } επιλέχθηκαν", + "type": "Τύπος", + "key": "Κλειδί", + "value": "Τιμή", + "id": "ID", + "extension-id": "ID επέκτασης", + "extension-type": "Τύπος επέκτασης", + "transformer-json": "JSON *", + "unique-id-required": "Το τρέχον ID επέκτασης υπάρχει ήδη.", + "delete": "Διαγραφή επέκτασης", + "add": "Προσθήκη επέκτασης", + "edit": "Επεξεργασία επέκτασης", + "view": "Προβολή επέκτασης", + "delete-extension-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την επέκταση '{{extensionId}}';", + "delete-extension-text": "Προσοχή, μετά την επιβεβαίωση, η επέκταση και όλα τα σχετικά δεδομένα θα διαγραφούν μόνιμα.", + "delete-extensions-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Προσέξτε, μετά την επιβεβαίωση θα αφαιρεθούν όλες οι επιλεγμένες επεκτάσεις.", + "converters": "Μετατροπείς", + "converter-id": "ID Μετατροπέα", + "configuration": "Διαμόρφωση", + "converter-configurations": "Ρυθμίσεις μετατροπέα", + "token": "Ετικέτα ασφαλείας", + "add-converter": "Προσθήκη μετατροπέα", + "add-config": "Προσθήκη ρυθμήσεων μετατροπέα", + "device-name-expression": "Έκφραση ονόματος συσκευής", + "device-type-expression": "Έκφραση τύπου συσκευής", + "custom": "Προσαρμοσμένο", + "to-double": "Διπλασιασμός", + "transformer": "Μετατροπέας", + "json-required": "Απαιτείται μετατροπέας JSON.", + "json-parse": "Αδύνατον να γίνει αναλύση του μετατροπέα JSON.", + "attributes": "Χαρακτηριστικά", + "add-attribute": "Προσθήκη χαρακτηριστικού", + "add-map": "Προσθήκη στοιχείου χαρτογράφισης", + "timeseries": "Χρονική σειρά", + "add-timeseries": "Προσθήκη χρονικής σειράς", + "field-required": "Απαιτείται το πεδίο", + "brokers": "Brokers", + "add-broker": "Προσθήκη broker", + "host": "Host", + "port": "Port", + "port-range": "Η Port πρέπει να είναι από 1 ως 65535.", + "ssl": "Ssl", + "credentials": "Διαπιστευτήρια", + "username": "Όνομα Χρήστη", + "password": "Κωδικός", + "retry-interval": "Διάστημα επανάληψης σε χιλιοστά του δευτερολέπτου", + "anonymous": "Ανώνυμα", + "basic": "Βασικά", + "pem": "PEM", + "ca-cert": "Αρχείο πιστοποιητικού CA *", + "private-key": "Αρχείο ιδιωτικού κλειδιού *", + "cert": "Αρχείο πιστοποιητικού *", + "no-file": "Δεν έχει επιλεχθεί αρχείο.", + "drop-file": "Αποθέστε ένα αρχείο ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "mapping": "χαρτογράφιση", + "topic-filter": "Φίλτρο θέματος", + "converter-type": "Τύπος μετατροπέα", + "converter-json": "JSON", + "json-name-expression": "Έκφραση json ονόματος συσκευής", + "topic-name-expression": "Έκφραση θέματος ονόματος συσκευής", + "json-type-expression": "Έκφραση json τύπου συσκευής", + "topic-type-expression": "Έκφραση θέματος τύπου συσκευής", + "attribute-key-expression": "Έκφραση χαρακτηριστικού κλειδιού", + "attr-json-key-expression": "Έκφραση JSON χαρακτηριστικού κλειδιού", + "attr-topic-key-expression": "Έκφραση θέματος χαρακτηριστικού κλειδιού", + "request-id-expression": "Αίτημα έκφρασης ID", + "request-id-json-expression": "Αίτημα ID έκφρασης JSON", + "request-id-topic-expression": "Αίτημα ID έκφρασης θέματος", + "response-topic-expression": "Απάντηση έκφρασης θέματος", + "value-expression": "Έκφραση τιμής", + "topic": "Θέμα", + "timeout": "Λήξη σε χιλιοστά του δευτερολέπτου", + "converter-json-required": "Απαιτείται μετατροπέας JSON.", + "converter-json-parse": "Αδύνατον να αναλυθεί ο μετατροπέας JSON.", + "filter-expression": "Έκφραση φίλτρου", + "connect-requests": "Αιτήματα σύνδεσης", + "add-connect-request": "Προσθήκη αιτήματος σύνδεσης", + "disconnect-requests": "Αιτήματα αποσύνδεσης", + "add-disconnect-request": "Προσθήκη αιτήματος αποσύνδεσης", + "attribute-requests": "Χαρακτιριστικό αιτημάτων", + "add-attribute-request": "Προσθήκη χαρακτηριστικού αιτήματος", + "attribute-updates": "Ανανεώσεις χαρακτηριστικού", + "add-attribute-update": "Προσθήκη ανανέωσης χαρακτηριστικού", + "server-side-rpc": "Server side RPC", + "add-server-side-rpc-request": "Προσθήκη αιτήματος server-side RPC", + "device-name-filter": "Φίλτρο ονόματος συσκευής", + "attribute-filter": "ίλτρο χαρακτηριστικού", + "method-filter": "Φίλτρο μεθόδου", + "request-topic-expression": "Αίτημα έκφρασης θέματος", + "response-timeout": "Λήξη απάντησης σε χιλιοστά του δευτερολέπτου", + "topic-expression": "Έκφρασου θέματος", + "client-scope": "Πεδίο εφαρμογής πελάτη", + "add-device": "Προσθήκη συσκευής", + "opc-server": "Servers", + "opc-add-server": "Προσθήκη server", + "opc-add-server-prompt": "Παρακαλούμε προσθέστε server", + "opc-application-name": "Όνομα εφαρμογής", + "opc-application-uri": "URI Εφαρμογής", + "opc-scan-period-in-seconds": "Περίοδος σάρωσης σε δευτερόλεπτα", + "opc-security": "Ασφάλεια", + "opc-identity": "Ταυτότητα", + "opc-keystore": "Keystore", + "opc-type": "Τύπος", + "opc-keystore-type": "Τύπος", + "opc-keystore-location": "τοποθεσία *", + "opc-keystore-password": "Κωδικός", + "opc-keystore-alias": "Ψευδώνυμο", + "opc-keystore-key-password": "Κωδικός Κλειδί", + "opc-device-node-pattern": "Μοτίβο κόμβου συσκευής", + "opc-device-name-pattern": "Μοτίβο ονόματος συσκευής", + "modbus-server": "Servers/slaves", + "modbus-add-server": "Προσθήκη server/slave", + "modbus-add-server-prompt": "Παρακαλούμε προσθέστε server/slave", + "modbus-transport": "Μεταφορά", + "modbus-tcp-reconnect": "Αυτόματη επανασύνδεση", + "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-port-name": "Όνομα serial port", + "modbus-encoding": "Encoding", + "modbus-parity": "Parity", + "modbus-baudrate": "Baud rate", + "modbus-databits": "Data bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Τα Data bits πρέπει να κυμαίνονται από 7 εώς 8.", + "modbus-stopbits-range": "Τα Stop bits πρέπει να κυμαίνονται από 1 εώς 2.", + "modbus-unit-id": "ID Μονάδας", + "modbus-unit-id-range": "Το ID Μονάδας πρέπει να κυμαίνεται από 1 εώς 247.", + "modbus-device-name": "Όνομα συσκευής", + "modbus-poll-period": "Poll period (ms)", + "modbus-attributes-poll-period": "Χαρακτηριστικά poll period (ms)", + "modbus-timeseries-poll-period": "Χρονική σειρά poll period (ms)", + "modbus-poll-period-range": "Το Poll period θα πρέπει να έχει θετική τιμή.", + "modbus-tag": "Ετικέτα", + "modbus-function": "Λειτουργία", + "modbus-register-address": "Καταχώριση διεύθυνσης", + "modbus-register-address-range": "η καταχωριμένη διεύθυνση πρεπει να κυμαίνεται από 0 ως 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "Το Bit index πρέπει να κυμαίνεται από 0 ως 15.", + "modbus-register-count": "Μετρητής καταχώρησης", + "modbus-register-count-range": "Ο μετρητής καταχώρησης πρεέπει να έχει θετική τιμή.", + "modbus-byte-order": "Byte order", + "sync": { + "status": "Κατάσταση", + "sync": "Σε Συγχρονισμό", + "not-sync": "Δεν Συγχρονίζεται", + "last-sync-time": "Τελευταία φορά συγχρονισμού", + "not-available": "Μη διαθέσιμο" + }, + "export-extensions-configuration": "Εξαγωγή διαμόρφωσης επεκτάσεων", + "import-extensions-configuration": "Εισαγωγή διαμόρφωσης επεκτάσεων", + "import-extensions": "Εισαγωγή επεκτάσεων", + "import-extension": "Εισαγωγή επέκτασης", + "export-extension": "Εξαγωγή επέκτασης", + "file": "Επεκτάσεις αρχείου", + "invalid-file-error": "Μη έγκυρη επέκταση αρχείου" + }, + "fullscreen": { + "expand": "Ανάπτυξη σε πλήρη οθόνη", + "exit": "Έξοδος από πλήρη οθόνη", + "toggle": "Εναλλαγή σε πλήρη οθόνη", + "fullscreen": "Πλήρης οθόνη" + }, + "function": { + "function": "Λειτουργία" + }, + "grid": { + "delete-item-title": "Είστε σίγουροι ότι θέλετε να διαγραφεί αυτό το αντικείμενο?", + "delete-item-text": "Προσοχή, μετά την επιβεβαίωση, αυτό το στοιχείο και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-items-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "Διαγραφή { count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "Προσοχή, μετά την επιβεβαίωση, όλα τα επιλεγμένα στοιχεία θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "add-item-text": "Προσθήκη νέου αντικειμένου", + "no-items-text": "Δεν βρέθηκαν αντικείμενα", + "item-details": "λεπτομέρειες αντικείμενου", + "delete-item": "Διαγραφή Αντικειμένου", + "delete-items": "Διαγραφή Αντικειμένων", + "scroll-to-top": "Κύλιση προς τα πάνω" + }, + "help": { + "goto-help-page": "Πηγαίνετε στη σελίδα" + }, + "home": { + "home": "Αρχικη", + "profile": "Προφίλ", + "logout": "Αποσυνδέση", + "menu": "Μενού", + "avatar": "Avatar", + "open-user-menu": "Άνοιγμα μενού χρήστη" + }, + "import": { + "no-file": "Δεν έχει επιλεχθεί αρχείο", + "drop-file": "Αποθέστε ένα αρχείο JSON ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "drop-csv-file": "Αποθέστε ένα αρχείο CVS ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "drop-file-csv": "Αποθέστε ένα αρχείο CSV ή κάνετε κλικ για να επιλέξετε ένα αρχείο για ανέβασμα.", + "column-value": "Τιμή", + "column-title": "Τίτλος", + "column-example": "Παράδειγμα δεδομένων", + "column-key": "Κλειδί χαρακτηριστικού/τηλεμετρίας", + "csv-delimiter": "Οριοθέτηση CSV", + "csv-first-line-header": "Η πρώτη σειρά περιέχει ονόματα στηλών", + "csv-update-data": "Ανανέωση χαρακτηριστικών/τηλεμετρίας", + "import-csv-number-columns-error": "Ένα αρχείο θα πρέπει να περιέχει τουλάχιστον δυο στήλες", + "import-csv-invalid-format-error": "Μη έγκυρη μορφή αρχείου. Σειρά: '{{line}}'", + "column-type": { + "name": "Όνομα", + "type": "Τύπος", + "column-type": "Τύπος στήλης", + "client-attribute": "Χαρακτηριστικό Client", + "shared-attribute": "Χαρακτηριστικό Shared", + "server-attribute": "Χαρακτηριστικό Server", + "timeseries": "Χρονική σειρά", + "entity-field": "Πεδίο Οντότητας", + "access-token": "Διακριτικό πρόσβασης" + }, + "stepper-text": { + "select-file": "Επιλογή αρχείου", + "configuration": "Εισαγωγή ρύθμισης", + "column-type": "Επιλογή τύπου στήλης", + "creat-entities": "Δημιουργία νέων οντοτήτων", + "done": "Ολοκληρώθηκε" + }, + "message": { + "create-entities": "{{count}} νέες οντότητες δημιουργήθηκαν με επιτυχία.", + "update-entities": "{{count}} οντότητες ενημερώθηκαν με επιτυχία.", + "error-entities": "Υπήρξε κάποιο σφάλμα κατά τη δημιουργία {{count}} οντοτήτων." + } + }, + "integration": { + "integration": "Ενσωμάτωση", + "integrations": "Ενσωματώσεις", + "select-integration": "Επιλογή ενσωμάτωσης", + "no-integrations-matching": "Δεν βρέθηκαν ενσωματώσεις πο να αντιστοιχούν σε '{{entity}}'.", + "integration-required": "Απαιτείται ενσωμάτωση", + "delete": "Διαγραφή ενσωμάτωσης", + "management": "Διαχείριση Ενσωματώσεων", + "add-integration-text": "Προσθήκη νέας ενσωμάτωσης", + "no-integrations-text": "Δεν βρέθηκαν ενσωματώσεις", + "selected-integrations": "{ count, plural, 1 {1 integration} other {# integrations} } επιλέχθηκαν", + "delete-integration-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την ενσωμάτωση '{{integrationName}}';", + "delete-integration-text": "Προσοχή, μετά την επιβεβαίωση, η ενσωμάτωση και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-integrations-title": "Είστε σίγουροι ότι θέλετε { count, plural, 1 {1 integration} other {# integrations} };", + "delete-integrations-action-title": "Διαγραφή { count, plural, 1 {1 integration} other {# integrations} }", + "delete-integrations-text": "Προσοχή, αφού ολοκληρωθεί η επιβεβαίωση, όλες οι επιλεγμένες ενσωματώσεις θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "events": "Γεγονότα", + "add": "Προσθήκη Ενσωμάτωσης", + "integration-details": "Λεπτομέρειες Ενσωμάτωσης", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραη ID ενσωμάτωσης", + "idCopiedMessage": "Το ID της ενσωμάτωσης έχει αντιγραφεί στο πρόχειρο.", + "debug-mode": "Λειτουργία απασφαλμάτωσης", + "enable-security": "Ενεργοποίηση ασφάλειας", + "headers-filter": "Φίλτρο Headers", + "header": "Header", + "no-headers-filter": "Όχι φίλτρο headers", + "downlink-url": "URL Λήψης", + "application-uri": "URI Εφαρμογής", + "as-id": "AS ID", + "as-id-required": "Απαιτείται AS ID.", + "as-key": "AS Key", + "as-key-required": "Απαιτείται AS Key.", + "max-time-diff-in-seconds": "Μέγιστη χρονική διαφορά (δευτερόλεπτα)", + "max-time-diff-in-seconds-required": "Απαιτείται μέγιστη χρονική διαφορά.", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "base-url": "Base URL", + "base-url-required": "Απαιτείται Base URL", + "security-key": "Κλειδί ασφάλειας", + "http-endpoint": "HTTP endpoint URL", + "copy-http-endpoint-url": "Αντιγραφή HTTP endpoint URL", + "http-endpoint-url-copied-message": "Το HTTP endpoint URL έχει αντιγραφεί στο πρόχειρο", + "host": "Host", + "host-required": "Απαιτείται Host.", + "host-type": "Τύπος Host", + "host-type-required": "Απαιτείται τύπος Host.", + "custom-host": "Προσαρμοσμένο host", + "custom-host-required": "Απαιτείται προσαρμοσμένο host.", + "port": "Port", + "port-required": "Απαιτείται Port.", + "port-range": "Η Port πρέπει να κυμαίνεται από 1 ως 65535.", + "connect-timeout": "Λήξη σύνδεσης (δευτερόλεπτα)", + "connect-timeout-required": "Απαιτείται λήξη σύνδεσης.", + "connect-timeout-range": "Η λήξη σύνδεσης πρέπει να κυμαίνεται από 1 ως 200.", + "client-id": "Client ID", + "clean-session": "Καθαρή συνεδρία session", + "enable-ssl": "Ενεργοποίηση SSL", + "credentials": "Διαπιστευτήρια", + "credentials-type": "Τύπος διαπιστευτηρίων", + "credentials-type-required": "Απαιτείται τύπος διαπιστευτηρίων.", + "username": "Όνομα Χρήστη", + "username-required": "Απαιτείται όνομα χρήστη.", + "password": "Κωδικός", + "password-required": "Απαιτείται κωδικός.", + "ca-cert": "Αρχείο πιστοποιητικού CA *", + "private-key": "Αρχείο ιδιωτικού κλειδιού *", + "private-key-password": "κωδικός ιδιωτικού κλειδιού", + "cert": "Αρχείο πιστοποιητικού *", + "no-file": "Δεν έχει επιλεχθεί αρχείο.", + "drop-file": "Αποθέστε αρχείο ή κάνετε κλικ για να επιλέξετε ένα αρχείο προς ανέβασμα.", + "topic-filters": "Φίτρο θέματος", + "remove-topic-filter": "Αφαίρεση φίλτρου θέματος", + "add-topic-filter": "προσθήκη φίλτρου θέματος", + "add-topic-filter-prompt": "παρακαλούμε προσθέστε φίλτρο θέματος", + "topic": "Θέμα", + "mqtt-qos": "QoS", + "mqtt-qos-at-most-once": "Το πολύ μια", + "mqtt-qos-at-least-once": "Τουλάχιστον μια", + "mqtt-qos-exactly-once": "Ακριβώς μια", + "downlink-topic-pattern": "Μοτίβο θέματος σύνδεσης", + "downlink-topic-pattern-required": "Απαιτείται μοτίβο θέματος σύνδεσης.", + "aws-iot-endpoint": "AWS IoT Endpoint", + "aws-iot-endpoint-required": "Απαιτείται AWS IoT Endpoint.", + "aws-iot-credentials": "AWS IoT Διαπιστευτήρια", + "application-credentials": "Διαπιστευτήρια Εφαρμογής", + "api-key": "API Key", + "api-key-required": "Απαιτείται API Key.", + "auth-token": "Τεκμίριο Ταυτοποίησης", + "auth-token-required": "Απαιτείται τεκμίριο ταυτοποίησης", + "region": "Περιοχή", + "region-required": "Απαιτείται περιοχή.", + "application-id": "ID Εφαρμογής", + "application-id-required": "Απαιτείται ID εφαρμογής.", + "access-key": "Κλειδί πρόσβασης", + "access-key-required": "Απαιτείται κλειδί πρόσβασης.", + "connection-parameters": "Παράμετροι σύνδεσης", + "service-bus-namespace-name": "Service Bus Namespace Name", + "service-bus-namespace-name-required": "Απαιτείται Service Bus Namespace Name.", + "event-hub-name": "Όνομα Event Hub", + "event-hub-name-required": "Απαιτείται όνομα Event Hub.", + "sas-key-name": "Όνομα SAS Key", + "sas-key-name-required": "Απαιτείται όνομα SAS Key.", + "sas-key": "SAS Key", + "sas-key-required": "Απαιτείται SAS Key.", + "iot-hub-name": "IoT Hub Name (απαιτείται για downlink)", + "metadata": "Μεταδεδομένα", + "type": "Τύπος", + "type-required": "Απαιτείται τύπος.", + "uplink-converter": "Μετατροπέας δεδομένων uplink", + "uplink-converter-required": "Απαιτείται μετατροπέας δεδομένων uplink.", + "downlink-converter": "Μετατροπέας δεδομένων downlink", + "type-http": "HTTP", + "type-ocean-connect": "OceanConnect", + "type-sigfox": "SigFox", + "type-thingpark": "ThingPark", + "type-tmobile-iot-cdp": "T-Mobile – IoT CDP", + "type-mqtt": "MQTT", + "type-aws-iot": "AWS IoT", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ttn": "TheThingsNetwork", + "type-azure-event-hub": "Azure Event Hub", + "type-opc-ua": "OPC-UA", + "opc-ua-application-name": "Όνομα εφαρμογής", + "opc-ua-application-uri": "URI Εφαρμογής", + "opc-ua-scan-period-in-seconds": "Περίοδος σάρωσης σε δευτερόλεπτα", + "opc-ua-scan-period-in-seconds-required": "Απαιτείται περίοδος σάρωσης", + "opc-ua-timeout": "Λήξη χρόνου σε χιλιοστά του δευτερολέπτου", + "opc-ua-timeout-required": "Απαιτείται λήξη χρόνου", + "opc-ua-security": "Ασφάλεια", + "opc-ua-security-required": "Απαιτειται ασφάλεια", + "opc-ua-identity": "Ταυτότητα", + "opc-ua-identity-required": "Απαιτείται ταυτότητα", + "opc-ua-keystore": "Keystore", + "add-opc-ua-keystore-prompt": "Παρακαλούμε προσθέστε αρχείο keystore", + "opc-ua-keystore-required": "Απαιτείται keystore", + "opc-ua-type": "Τύπος", + "opc-ua-keystore-type": "Τύπος", + "opc-ua-keystore-type-required": "Απαιτέιται τύπος", + "opc-ua-keystore-location": "Τοποθεσία *", + "opc-ua-keystore-password": "Κωδικός", + "opc-ua-keystore-password-required": "Απαιτείται κωδικός", + "opc-ua-keystore-alias": "Ψευδώνυμο", + "opc-ua-keystore-alias-required": "Απαιτείται ψευδώνυμο", + "opc-ua-keystore-key-password": "Κλειδί κωδικού", + "opc-ua-keystore-key-password-required": "Απαιτείται κλειδί κωδικού", + "opc-ua-mapping": "χαρτογράφιση", + "add-opc-ua-mapping-prompt": "Παρακαλούμε προσθέστε χαρτογράφιση", + "opc-ua-mapping-type": "Τύπος χαρτογράφισης", + "opc-ua-mapping-type-required": "Απαιτείται τύπος χαρτογράφισης", + "opc-ua-device-node-pattern": "Μοτίβο Κόμβου Συσκευής", + "opc-ua-device-node-pattern-required": "Απαιτείται Μοτίβο Κόμβου Συσκευής", + "opc-ua-namespace": "Namespace", + "opc-ua-add-map": "Προσθήκη στοιχείου χαρτογράφισης", + "subscription-tags": "Ετικέτες εγγραφής", + "remove-subscription-tag": "Αφαίρεση ετικέτας εγγραφής", + "add-subscription-tag": "Προσθήκη ετικέτας εγγραφής", + "add-subscription-tag-prompt": "Παρακαλούμε προσθέστε ετικέτα εγγραφής", + "key": "Κλειδί", + "path": "Μονοπάτι", + "required": "Απαιτείται" + }, + "item": { + "selected": "Επιλέχθηκε" + }, + "js-func": { + "no-return-error": "Η λειτουργία πρέπει να επιστρέφει τιμή!", + "return-type-mismatch": "Η λειτουργία πρέπει να επιστρέφει τιμή από '{{type}}' τύπο!", + "tidy": "Tidy" + }, + "key-val": { + "key": "Όνομα", + "value": "Τιμή", + "remove-entry": "Αφαίρεση καταχώρησης", + "add-entry": "Προσθήκη καταχωρησης", + "no-data": "Καμία καταχώρηση" + }, + "layout": { + "layout": "Διάταξη", + "manage": "Διαχείριση Διατάξεων", + "settings": "Ρυθμίσεις Διάταξης", + "color": "Χρώμα", + "main": "Κεντρικά", + "right": "Δεξιά", + "select": "Επιλογή διάταξης" + }, + "legend": { + "direction": "Κατεύθυνση Λεζάντας", + "position": "Θέση λεζάντας", + "show-max": "Προβολή μέγιστης τιμής", + "show-min": "Προβολή ελάχιστης τιμής", + "show-avg": "Προβολή μέσης τιμής", + "show-total": "Προβολή συνολικής τιμής", + "settings": "Ρυθμίσεις λεζάντας", + "min": "min", + "max": "max", + "avg": "Μ.Ο.", + "total": "Σύνολο" + }, + "login": { + "login": "Σύνδεση", + "request-password-reset": "Αίτημα επαναφοράς κωδικού πρόσβασης", + "reset-password": "Επαναφορά κωδικού πρόσβασης", + "create-password": "Δημιουργία κωδικού πρόσβασης", + "passwords-mismatch-error": "Οι καταχωρημένοι κωδικοί πρόσβασης πρέπει να είναι ίδιοι!", + "password-again": "Επανάληψη κωδικού πρόσβασης", + "sign-in": "Παρακαλώ, συνδεθείτε", + "username": "Όνομα Χρήστη (email)", + "remember-me": "Να με Θυμάσαι", + "forgot-password": "Ξεχάσατε τον κωδικό πρόσβασης;", + "password-reset": "Επαναφορά κωδικού πρόσβασης", + "new-password": "Νέος κωδικός πρόσβασης", + "new-password-again": "Επανάληψη νέου κωδικού πρόσβασης", + "password-link-sent-message": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης στάλθηκε με επιτυχία!", + "email": "Email", + "no-account": "Δεν έχετε λογαριασμό;", + "create-account": "Δημιουργία λογαριασμού" + }, + "signup": { + "firstname": "Όνομα", + "lastname": "Επίθετο", + "email": "Email", + "signup": "Εγγραφή", + "create-password": "Δημιουργία κωδικού", + "repeat-password": "Επαναλάβετε τον κωδικό", + "have-account": "Έχετε ήδη έναν λογαριασμό;", + "signin": "Είσοδος", + "no-captcha-message": "Πρέπει να επιβεβαιώσετε ότι δεν είστε ρομπότ", + "password-length-message": "Ο κωδικός σας πρεπει να περιλαμβάνει τουλάχιστον 6 χαρακτήρες", + "email-verification": "Επιβεβαίωση email", + "email-verification-message": "Έχει σταλεί ένα email επιβεβαίωσης στην διεύθυνση email που καθορίσατε.
Παρακαλούμε ακολουθήστε τις οδηγίες που συμπεριλαμβανονται στο email ώστε να ολοκληρωθεί η εγγραφή σας.
Σημείωση: Αν δεν έχει εμφανιστεί σχετικά άμεσα το email, παρακαλούμε ελέγξτε στον φάκελο με την 'Ανεπιθύμητη Αλληλογραφία' (spam) ή ξαναπροσπαθείστε να στείλετε το email κάνοντας κλικ στο κουμπί 'Αποστολή εκ νέου'", + "account-activation-title": "Ενεργοποίηση λογαριασμού", + "account-activated": "Ο λογαριασμός ενεργοποιήθηκε με επιτυχία!", + "account-activated-text": "Συγχαρητήρια!
Ο λογαριασμός σας έχει ενεργοποιηθεί.
Τώρα μπορείται να κάνετε είσοδο στην πλατφόρμα.", + "resend": "Αποστολή εκ νέου", + "inactive-user-exists-title": "Ο ανενεργός χρήστης υπάρχει ήδη", + "inactive-user-exists-text": "Υπάρχει ήδη εγγεγραμμένος χρήστης με μη επιβεβαιωμένη διεύθυνση email.
Click Πατήστε το κουμπί 'Επαναποστολή', αν θέλετε να στείλετε ξανά email επιβεβαίωσης.", + "activating-account": "Ενεργοποίηση λογαριασμού...", + "activating-account-text": "Ο λογαριασμός σας ενεργοποιείται αυτήν τη στιγμή. Παρακαλούμε περιμένετε...", + "accept-privacy-policy": "Αποδοχή Πολιτικής Απορρήτου", + "accept": "Αποδοχή", + "privacy-policy": "Πολιτική Απορρήτου" + }, + "position": { + "top": "Κορυφή", + "bottom": "Κάτω μέρος", + "left": "Αριστερά", + "right": "Δεξιά" + }, + "profile": { + "profile": "Προφίλ", + "change-password": "Αλλαγή κωδικού", + "current-password": "Τρέχων κωδικός" + }, + "relation": { + "relations": "Σχέσεις", + "direction": "Κατεύθυνση", + "search-direction": { + "FROM": "Από", + "TO": "Σε" + }, + "direction-type": { + "FROM": "από", + "TO": "σε" + }, + "from-relations": "Εξωτερικές σχέσεις", + "to-relations": "Εσωτερικές σχέσεις", + "selected-relations": "{ count, plural, 1 {1 relation} άλλες {# relations} } επιλέχθηκαν", + "type": "Τύπος", + "to-entity-type": "Στον τύπο οντότητας", + "to-entity-name": "Στο όνομα οντότητας", + "from-entity-type": "Από τύπο οντότητας", + "from-entity-name": "Από όνομα οντότητας", + "to-entity": "Σε οντότητα", + "from-entity": "Από οντότητα", + "delete": "Διαγραφή σχέσης", + "relation-type": "Relation type", + "relation-type-required": "Απαιτείται τύπος σχέσης.", + "any-relation-type": "Οποιοσδήποτε τύπος", + "add": "Προσθήκη σχέσης", + "edit": "Επεξεργασία σχέσης", + "delete-to-relation-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τη σχέση με την οντότητα '{{entityName}}'?", + "delete-to-relation-text": "Προσοχή, μετά την επιβεβαίωση η οντότητα '{{entityName}}' δεν θα σχετίζεται με την τρέχουσα οντότητα.", + "delete-to-relations-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 relation} άλλες {# relations} };", + "delete-to-relations-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες σχέσεις θα αφαιρεθούν και οι αντίστοιχες οντότητες δεν θα σχετίζονται με την τρέχουσα οντότητα.", + "delete-from-relation-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε σχέση από την οντότητα '{{entityName}}';", + "delete-from-relation-text": "Προσοχή, μετά την επιβεβαίωση η τρέχουσα οντότητα δεν θα σχετίζεται με την οντότητα '{{entityName}}'.", + "delete-from-relations-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 relation} other {# relations} };", + "delete-from-relations-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες σχέσεις θα αφαιρεθούν και η τρέχουσα οντότητα δεν θα σχετίζεται με τις αντίστοιχες οντότητες.", + "remove-relation-filter": "Αφαίρεση φίλτρου σχέσης", + "add-relation-filter": "Προσθήκη φίλτρου σχέσης", + "any-relation": "Οποιαδήποτε σχέση", + "relation-filters": "Φίλτρα σχέσεων", + "additional-info": "Συμπληρωματικές πληροφορίες (JSON)", + "invalid-additional-info": "Δεν είναι δυνατή η ανάλυση των πρόσθετων πληροφοριών json." + }, + "rulechain": { + "rulechain": "Αλυσίδα Κανόνων", + "rulechains": "Κανόνες", + "root": "Ρίζα", + "delete": "Διαγραφή Αλυσίδας Κανόνων", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "add": "Προσθήκη Αλυσίδας Κανόνων", + "set-root": "Δημιουργία ριζικής Αλυσίδας Κανόνων", + "set-root-rulechain-title": "Είστε σίγουροι ότι θέλετε να δημιουργήσετε τη ριζική Αλυσίδα Κανόνων '{{ruleChainName}}' ;", + "set-root-rulechain-text": "Μετά την επιβεβαίωση, η Αλυσίδα Κανόνων θα γίνει ριζική και θα χειριστεί όλα τα εισερχόμενα μηνύματα μεταφοράς.", + "delete-rulechain-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την Αλυσίδα Κανόνων '{{ruleChainName}}';", + "delete-rulechain-text": "Προσοχή, μετά την επιβεβαίωση η Αλυσίδα Κανόνων θα αφαιρεθεί και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-rulechains-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 rule chain} other {# rule chains} };", + "delete-rulechains-action-title": "Διαγραφή { count, plural, 1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες αλυσίδες κανόνων θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "add-rulechain-text": "Προσθήκη νέας Αλυσίδας Κανόνων", + "no-rulechains-text": "Δεν βρέθηκαν Αλυσίδες Κανόνων", + "rulechain-details": "Λεπτομέρειες Αλυσίδας Κανόνων", + "details": "Λεπομέρειες", + "events": "Γεγονότα", + "system": "Σύστημα", + "import": "Εισαγωγή Αλυσίδας Κανόνων", + "export": "Εξαγωγή Αλυσίδας Κανόνων", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή Αλυσίδας Κανόνων: {{error}}", + "create-new-rulechain": "Δημιουργία νέας Αλυσίδας Κανόνων", + "rulechain-file": "Αρχείο Αλυσίδας Κανόνων", + "invalid-rulechain-file-error": "Δεν είναι δυνατή η εισαγωγή Αλυσίδας Κανόνων: Μη έγκυρη δομή δεδομένων Αλυσίδας Κανόνων.", + "copyId": "Αντιγραφή ταυτότητας Αλυσίδας Κανόνων", + "idCopiedMessage": "Η ταυτότητα της Αλυσίδας Κανόνων έχει αντιγραφεί στο πρόχειρο", + "select-rulechain": "Επιλογή Αλυσίδας Κανόνων", + "no-rulechains-matching": "Δεν βρέθηκαν Αλυσίδες Κανόνων που να ταιριάζουν '{{entity}}'.", + "rulechain-required": "Απαιτείται Αλυσίδα Κανόνων", + "management": "Διαχείριση κανόνων", + "debug-mode": "Λειτουργία Εκσφαλμάτωσης" + }, + "rulenode": { + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "search": "Αναζήτηση κόμβων", + "open-node-library": "Άνοιγμα βιβλιοθήκης κόμβων", + "add": "Προσθήκη κανόνα κόμβου", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "type": "Τύπος", + "description": "Περιγραφή", + "delete": "Διαγραφή κανόνα κόμβου", + "select-all-objects": "Επιλογή όλων των κόμβων και συνδέσεων", + "deselect-all-objects": "Αποεπιλογή όλων των κόμβων και συνδέσεων", + "delete-selected-objects": "Διαγραφή επιλεγμένων κόμβων και συνδέσεων", + "delete-selected": "Delete selected", + "select-all": "Select all", + "copy-selected": "Επιλογή αντιγραφής", + "deselect-all": "Αποεπιλογή όλων", + "rulenode-details": "Λεπτομέρειες κανόνα κόμβου", + "debug-mode": "Λειτουργία εντοπισμού σφαλμάτων", + "configuration": "Διαμόρφωση", + "link": "Σύνδεσμος", + "link-details": "Λεπτομέρειες συνδέσμου κανόνα κόμβου", + "add-link": "Προσθήκη συνδέσμου", + "link-label": "Ετικέτα συνδέσμου", + "link-label-required": "Απαιτείται ετικετα συνδέσμου.", + "custom-link-label": "Ετικέτα προσαρμοσμένου συνδέσμου", + "custom-link-label-required": "Απαιτείται ετικέτα προσαρμοσμένου συνδέσμου.", + "link-labels": "Ετικέτες συνδέσμου", + "link-labels-required": "Απαιτούνται ετικέτες συνδέσμου.", + "no-link-labels-found": "Δεν βρέθηκαν ετικέτες συνδέσμου", + "no-link-label-matching": "Δεν βρέθηκαν '{{label}}'.", + "create-new-link-label": "Δημιουργήστε μια νέα!", + "type-filter": "Φίλτρο", + "type-filter-details": "Φιλτράρετε εισερχόμενα μηνύματα με διαμορφωμένες συνθήκες", + "type-enrichment": "Εμπλουτισμός", + "type-enrichment-details": "Προσθέστε επιπλέον πληροφορίες στα Μεταδεδομένα του μηνύματος", + "type-transformation": "Μεταμόρφωση", + "type-transformation-details": "Αλλαγή ωφέλιμου φορτίου και μεταδεδομένων μηνύματος", + "type-action": "Ενέργεια", + "type-action-details": "Εκτέλεση ειδικής ενέργειας", + "type-analytics": "Analytics", + "type-analytics-details": "Εκτελέστε ανάλυση δεδομένων που διαβιβάζονται με ροή ή συνεχίζονται", + "type-external": "Εξωτερικός", + "type-external-details": "Αλληλεπίδραση με εξωτερικό σύστημα", + "type-rule-chain": "Αλυσίδα κανόνων", + "type-rule-chain-details": "Προωθεί τα εισερχόμενα μηνύματα σε συγκεκριμένη αλυσίδα κανόνων", + "type-input": "Εισαγωγήγή", + "type-input-details": "Λογική εισαγωγή της αλυσίδας κανόνων, προωθεί τα εισερχόμενα μηνύματα στον επόμενο σχετικό κανόνα κόμβου", + "type-unknown": "Άγνωστο", + "type-unknown-details": "Ανεπεξέργαστος κανόνας κόμβου", + "directive-is-not-loaded": "Δεν είναι διαθέσιμη καθορισμένη οδηγία διαμόρφωσης '{{directiveName}}'.", + "ui-resources-load-error": "Αποτυχία φόρτωσης διαμόρφωσης πόρων ui.", + "invalid-target-rulechain": "Δεν είναι δυνατή η επίλυση της αλυσίδας κανόνα στόχου!", + "test-script-function": "Λειτουργία σεναρίου δοκιμής", + "message": "Μήνυμα", + "message-type": "Τύπος μηνύματος", + "select-message-type": "Επιλογή τύπου μηνύματος", + "message-type-required": "Απαιτείται τύπος μηνύματος", + "metadata": "Μεταδεδομένα", + "metadata-required": "Οι καταχωρίσεις μεταδεδομένων δεν μπορούν να είναι κενές.", + "output": "Απόδοση", + "test": "Τεστ", + "help": "Βοήθεια", + "reset-debug-mode": "Επαναφορά λειτουργίας εντοπισμού σφαλμάτων σε όλους τους κόμβους" + }, + "role": { + "role": "Ρόλος", + "roles": "Ρόλοι", + "management": "Διαχείριση ρόλων", + "view-roles": "Προβολή ρόλων", + "no-roles-matching": "Δεν βρέθηκαν ρόλοι που να ταιριάζουν '{{entity}}'.", + "role-list": "Λίστα ρόλων", + "add": "Προσθήκη ρόλου", + "view": "Προβολή ρόλου", + "no-roles-text": "Δεν βρέθηκαν ρόλοι", + "role-details": "Λεπτομέρειες ρόλου", + "add-role-text": "Προσθήκη νέου ρόλου", + "delete": "Διαγραφή ρόλου", + "delete-roles": "Διαγραφή ρόλων", + "delete-role-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το ρόλο '{{roleName}}';", + "delete-role-text": "Προσοχή, μετά την επιβεβαίωση ο ρόλος και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-roles-title": "Είστε σίγουροι ότι θέλετε να παίξετε { count, plural, 1 {1 role} other {# roles} };", + "delete-roles-action-title": "Διαγραφή { count, plural, 1 {1 role} other {# roles} }", + "delete-roles-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι ρόλοι θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "role-type": "΄Τύπος ρόλου", + "role-type-required": "Απαιτείται τύπος ρόλου.", + "select-role-type": "Επιλογή τύπου ρόλου", + "enter-role-type": "Εισαγωγή τύπου ρόλου", + "any-role": "Οποιοσδήποτε ρόλος", + "no-role-types-matching": "Δεν βρέθηκαν τύποι ρόλων που να ταιριάζουν '{{entitySubtype}}'.", + "role-type-list-empty": "Δεν έγινε επιλογή τύπου ρόλου.", + "role-types": "Τύποι ρόλου", + "name": "Όνομα", + "name-required": "Απαιτείται όνομα.", + "description": "Περιγραφή", + "events": "Γεγονότα", + "details": "Λεπτομέρειες", + "copyId": "Αντιγραφή ταυτότητας ρόλου", + "idCopiedMessage": "Η ταυτότητα ρόλου έχει αντιγραφεί στο πρόχειρο", + "permissions": "Άδειες", + "role-required": "Απαιτείται ρόλος", + "display-type": { + "GENERIC": "Γενικός", + "GROUP": "Ομάδα" + } + }, + "group-permission": { + "user-group-roles": "Ρόλοι ομάδας χρηστών", + "entity-group-permissions": "Άδειες ομάδας Οντοτήτων", + "role-type": "Τύπος ρόλου", + "role-name": "Όνομα ρόλου", + "group-type": "Τύπος ομάδας", + "group-name": "Όνομα ομάδας", + "group-owner": "Κάτοχος ομάδας", + "user-group-name": "Όνομα χρήστη ομάδας", + "user-group-owner": "Κάτοχος χρήστη ομάδας", + "edit": "Επεξεργασία Αδειών", + "delete": "Διαγραφή αδειών", + "selected-group-permissions": "{ count, plural, 1 {1 group permission} other {# group permissions} } επιλέχθηκαν", + "delete-group-permission-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε την άδεια ομάδας '{{roleName}}';", + "delete-group-permission-text": "Προσοχή, μετά την επιβεβαίωση η άδεια ομάδας και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-group-permission": "Διαγραφή άδειας ομάδας", + "delete-group-permissions-title": "Έίστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 group permission} other {# group permission} };", + "delete-group-permissions-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες άδειες ομάδας θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-group-permissions": "Διαγραφή αδειών ομάδας", + "add-group-permission": "Προσθήκη ομαδικής άδειας", + "edit-group-permission": "Επεξεργασία άδειας ομάδας", + "entity-group": "Ομάδα οντοτήτων", + "user-group": "Χρήστης ομάδας", + "no-owners-matching": "Δεν βρέθηκαν κάτοχοι που να ταιριάζουν '{{owner}}'.", + "target-owner-required": " Απαιτείται κάτοχος της ομάδας οντότητας.", + "target-user-group-owner-required": "Απαιτείται κάτοχος χρήστη ομάδας." + }, + "permission": { + "permissions-required": "Τουλάχιστον μία εγγραφή άδειας πρέπει να οριστεί.", + "remove-permission": "Καταργήστε την καταχώριση δικαιωμάτων", + "add-permission": "Προσθήκη καταχώρησης άδειας", + "resource": { + "resource": "Πηγή", + "select-resource": "Επιλογή πηγής", + "resource-required": "Απαιτείται πηγή", + "no-resources-matching": "Δεν βρέθηκαν πηγές που να ταιριάζουν '{{resource}}'.", + "display-type": { + "ALL": "Όλες", + "PROFILE": "Προφίλ", + "ADMIN_SETTINGS": "Ρυθμίσεις Διαχειριστή", + "ALARM": "Alarm", + "DEVICE": "Συσκευή", + "ASSET": "Asset", + "CUSTOMER": "Πελάτης", + "DASHBOARD": "Dashboard", + "ENTITY_VIEW": "Προβολή οντοτήτων", + "TENANT": "Μισθωτής", + "RULE_CHAIN": "Αλυσίδα Κανόνα", + "USER": "Χρήστης", + "WIDGETS_BUNDLE": "Δέσμη Widgets", + "WIDGET_TYPE": "Τύπος Widget", + "CONVERTER": "Μετατροπέας", + "INTEGRATION": "Ενσωμάτωση", + "SCHEDULER_EVENT": "Πρόγραμμα εκδηλώσεων", + "BLOB_ENTITY": "Δέσμη Οντότητας", + "CUSTOMER_GROUP": "Ομάδα Πελατών", + "DEVICE_GROUP": "Ομάδα Συσκευών", + "ASSET_GROUP": "Ομάδες Asset", + "USER_GROUP": "Ομάδα Χρηστών", + "ENTITY_VIEW_GROUP": "Ομάδα Οντοτήτων", + "DASHBOARD_GROUP": "Ομάδα Dashboard", + "ROLE": "Ρόλος", + "GROUP_PERMISSION": "Ομαδική Άδεια", + "WHITE_LABELING": "Εμφάνιση", + "AUDIT_LOG": "Αρχείο Ελέγχου" + } + }, + "operation": { + "operation": "Εργασία", + "operations": "Εργασίες", + "operations-required": "Τουλάχιστον μία εργασία πρέπει να καθοριστεί.", + "enter-operation": "Εισαγωγή εργασίας", + "no-operations-matching": "Δεν βρέθηκαν εργασίες που να ταιριάζουν '{{operation}}'.", + "display-type": { + "ALL": "'Ολες", + "CREATE": "Δημιουργία", + "READ": "Ανάγνωση", + "WRITE": "Εγγραφή", + "DELETE": "Διαγραφή", + "ASSIGN_TO_CUSTOMER": "Ανάθεση σε Πελάτη", + "UNASSIGN_FROM_CUSTOMER": "Αποσύνδεση από Πελάτη", + "RPC_CALL": "Κλήση RPC", + "READ_CREDENTIALS": "Ανάγνωση Διαπιστευτηρίων", + "WRITE_CREDENTIALS": "Γράψτε Διαπιστευτήρια", + "READ_ATTRIBUTES": "Ανάγνωση Χαρακτηριστικών", + "WRITE_ATTRIBUTES": "Γράψτε Χαρακτηριστικά", + "READ_TELEMETRY": "Ανάγνωση Τηλεμετρίας", + "WRITE_TELEMETRY": "Γράψτε Τηλεμετρία", + "CLAIM_DEVICES": "Αιτήματα Συσκευών", + "IMPERSONATE": "Impersonate", + "CHANGE_OWNER": "Αλλαγή Κατόχου", + "ADD_TO_GROUP": "Προσθήκη στην Ομάδα", + "REMOVE_FROM_GROUP": "Αφαίρεση από την Ομάδα" + } + } + }, + "scheduler": { + "scheduler": "Προγραμματιστής", + "scheduler-event": "Προγραμματισμένο γεγονός", + "select-scheduler-event": "Επιλέξτε προγραμματισμένα γεγονότα που να ταιριάζουν '{{entity}}'", + "scheduler-event-required": "Απαιτείται προγραμματισμένο γεγονός", + "management": "Διαχείριση Προγράμματος", + "scheduler-events": "Προγραμματισμένα γεγονότα", + "add-scheduler-event": "Προσθήκη προγραμματισμένου γεγονότος", + "search-scheduler-events": "Αναζήτηση προγραμματισμένων γεγονότων", + "created-time": "Χρόνος που δημιουργήθηκε", + "name": "Όνομα", + "type": "Τύπος", + "created_customer": "Πελάτης που δημιουργήθηκε", + "edit-scheduler-event": "Επεξεργασία προγραμματισμένου γεγονότος", + "view-scheduler-event": "Προβολή προγραμματισμένου γεγονότος", + "delete-scheduler-event": "Διαγραφή προγραμματισμένου γεγονότος", + "no-scheduler-events": "Δεν βρέθηκαν προγραμματισμένα γεγονότα", + "selected-scheduler-events": "{ count, plural, 1 {1 scheduler event} other {# scheduler events} } επιλέχθηκαν", + "delete-scheduler-event-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το προγραμματισμένο γεγονός '{{schedulerEventName}}';", + "delete-scheduler-event-text": "Προσοχή, μετά την επιβεβαίωση το προγραμματισμένο γεγονός και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-scheduler-events-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 scheduler event} other {# scheduler events} };", + "delete-scheduler-events-text": "Προσοχή, μετά την επιβεβαίωση όλα τα επιλεγμένα προγραμματισμένα γεγονότα θα καταργηθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "create": "Δημιουργία προγραμματισμένου γεγονότος", + "edit": "Επεξεργασία προγραμματισμένου γεγονότος", + "view": "Προβολή προγραμματισμένου γεγονότος", + "name-required": "Απαιτείται Όνομα", + "configuration": "Διαμόρφωση", + "schedule": "Πρόγραμμα", + "start": "Έναρξη", + "date": "Ημερομηνία", + "time": "Ώρα", + "repeat": "Επανάληψη", + "repeats": "Επαναλήψεις", + "daily": "Καθημερινά", + "weekly": "Εβδομαδιαία", + "timer": "Χρονομετρητής", + "repeats-required": "Απαιτούνται επαναλήψεις.", + "repeat-on": "Επανάληψη σε", + "repeat-every": "Επανάληψη κάθε", + "ends-on": "Τελειώνει σε", + "sunday-label": "K", + "monday-label": "Δ", + "tuesday-label": "T", + "wednesday-label": "Τ", + "thursday-label": "Π", + "friday-label": "Π", + "saturday-label": "Σ", + "repeat-on-sunday": "Επανάληψη την Κυριακή", + "repeat-on-monday": "Επανάληψη τη Δευτέρα", + "repeat-on-tuesday": "Επανάληψη την Τρίτη", + "repeat-on-wednesday": "Επανάληψη την Τετάρτη", + "repeat-on-thursday": "Επανάληψη την Πέμπτη", + "repeat-on-friday": "Επανάληψη την Παρασκευή", + "repeat-on-saturday": "Επανάληψη το Σάββατο", + "event-type": "Τύπος Γεγονότος", + "select-event-type": "Επιλογή Τύπου Γεγονότος", + "event-type-required": "Απαιτείται Τύπος Γεγονότος.", + "list-mode": "Προβολή Λίστας", + "calendar-mode": "Προβολή Ημερολογίου", + "calendar-view-type": "Προβολή τύπου ημερολογίου", + "month": "Μήνας", + "week": "Εβδομάδα", + "day": "Ημέρα", + "agenda-week": "Εβδομαδιαία Ατζέντα", + "agenda-day": "Ημερήσια Ατζέντα", + "list-year": "Λίστα Έτους", + "list-month": "Λίστα Μήνα", + "list-week": "Λίστα Εβδομάδος", + "list-day": "Λίστα Ημέρας", + "today": "Σήμερα", + "navigate-before": "Πλοηγηθείτε Πριν", + "navigate-next": "Πλοηγηθείτε Μετά", + "starting-from": "Έναρξη Από", + "until": "μέχρι", + "on": "σε", + "sunday": "Κυριακή", + "monday": "Δευτέρα", + "tuesday": "Τρίτη", + "wednesday": "Τετάρτη", + "thursday": "Πέμπτη", + "friday": "Παρασκευή", + "saturday": "Σάββατο", + "originator": "Δημιουργός" , + "single-entity": "Ενιαία Οντότητα", + "group-of-entities": "Ομάδα Οντοτήτων", + "single-device": "Ενιαία Συσκευή", + "group-of-devices": "Ομάδα Συσκευών", + "message-body": "Σώμα μηνυμάτων", + "target": "Στόχος", + "rpc-method": "Μέθοδος", + "rpc-method-required": "Απαιτείται μέθοδος", + "rpc-params": "Παράμετροι", + "select-dashboard-state": "Επιλογή κατάστασης πίνακα ελέγχου", + "hours": "Ώρες", + "minutes": "Λεπτά", + "seconds": "Δευτερόλεπτα", + "time-interval-required": "Απαιτείται χρονικό διάστημα", + "time-unit-required": "Απαιτείται μονάδα ώρας" + }, + "report": { + "report-config": "Διαμόρφωση αναφοράς", + "email-config": "Διαμόρφωση email", + "dashboard-state-param": "Τιμή παραμέτρου κατάστασης πίνακα", + "base-url": "Βάση URL", + "base-url-required": "Απαιτείται Βάση URL.", + "use-dashboard-timewindow": "Χρησιμοποιήστε το χρονικό πλαίσιο του πίνακα ελέγχου", + "timewindow": "Χρονικό πλαίσιο", + "name-pattern": "Αναφορά πρότυπου ονόματος", + "name-pattern-required": "Απαιτείται αναφορά πρότυπου ονόματος", + "type": "Τύπος αναφοράς", + "use-current-user-credentials": "Χρησιμοποιήστε τα τρέχοντα διαπιστευτήρια του χρήστη", + "customer-user-credentials": "Διαπιστευτήρια του χρήστη του πελάτη", + "customer-user-credentials-required": "Απαιτούνται διαπιστευτήρια του χρήστη του πελάτη", + "generate-test-report": "Δημιουργία αναφοράς δοκιμής", + "send-email": "Αποστολή email", + "from": "Από", + "from-required": "Απαιτείται Από.", + "to": "Σε", + "to-required": "Απαιτείται Σε.", + "cc": "Cc", + "bcc": "Bcc", + "subject": "Θέμα", + "subject-required": "Απαιτείται Θέμα.", + "body": "Σώμα", + "body-required": "Απαιτείται Σώμα." + }, + "blob-entity": { + "blob-entity": "Ογκώδης οντότητα", + "select-blob-entity": "Επιλογή ογκώδους οντότητας", + "no-blob-entities-matching": "Δεν βρέθηκαν ογκώδεις οντότητες που να ταιριάζουν '{{entity}}'.", + "blob-entity-required": "Απαιτείται ογκώδης οντότητα", + "files": "Αρχεία", + "search": "Αναζήτηση αρχείων", + "clear-search": "Εκκαθάριση αναζήτησης", + "no-blob-entities-prompt": "Δεν βρέθηκαν αρχεία", + "report": "Αναφορά", + "created-time": "Χρόνος που δημιουργήθηκε", + "name": "Όνομα", + "type": "Τύπος", + "created_customer": "Δημιουργήθηκε από Πελάτη", + "download-blob-entity": "Λήψη αρχείου", + "delete-blob-entity": "Διαγραφή αρχείου", + "delete-blob-entity-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το αρχείο '{{blobEntityName}}';", + "delete-blob-entity-text": "Προσοχή, μετά την επιβεβαίωση τα δεδομένα αρχείου θα διαγραφούν οριστικά." + }, + "timezone": { + "timezone": "Ζώνη Ώρας", + "select-timezone": "Επιλογή Ζώνης Ώρας", + "no-timezones-matching": "Δεν βρέθηκαν Ζώνες ώρας που να ταιριάζουν'{{timezone}}' .", + "timezone-required": "Απαιτείται Ζώνη Ώρας." + }, + "tenant": { + "tenant": "Μισθωτής", + "tenants": "Μισθωτές", + "management": "Διαχείριση Μισθωτών", + "add": "Πρόσθεση Μισθωτή", + "admins": "Διαχειριστές", + "manage-tenant-admins": "Επεξεργασία των διαχειριστών του Μισθωτή", + "delete": "Διαγραφή Μισθωτή", + "add-tenant-text": "Πρόσθεση νέου Μισθωτή", + "no-tenants-text": "Δεν βρέθηκαν Μισθωτές", + "tenant-details": "Λεπτομέρειες Μισθωτή", + "delete-tenant-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τον Μισθωτή '{{tenantTitle}}';", + "delete-tenant-text": "Προσοχή, μετά την επιβεβαίωση ο Μισθωτής και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-tenants-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 tenant} other {# tenants} };", + "delete-tenants-action-title": "Διαγραφή { count, plural, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι Μισθωτές θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "title": "Τίτλος", + "title-required": "Απαιτείται Τίτλος.", + "description": "Περιγραφή", + "details": "Λεπτομέρειες", + "events": "Γεγονότα", + "copyId": "Αντιγραφή Ταυτότητας του Μισθωτή", + "idCopiedMessage": "Η Ταυτότητα του Μισθωτή έχει αντιγραφεί στο πρόχειρο", + "select-tenant": "Επιλογή Μισθωτή", + "no-tenants-matching": "Δεν βρέθηκαν Μισθωτές που να ταιριάζουν '{{entity}}'.", + "tenant-required": "Απαιτείται Μισθωτής", + "selected-tenants": "{ count, plural, 1 {1 tenant} other {# tenants} } επιλέχθηκαν", + "search": "Αναζήτηση Μισθωτών", + "allow-white-labeling": "Επιτρέπεται Προσαρμογή Εμφάνισης", + "allow-customer-white-labeling": "Επιτρέπεται Προσαρμογή Εμφάνισης Πελάτη" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", + "days-interval": "{ days, plural, 1 {1 day} other {# days} }", + "days": "Ημέρες", + "hours": "Ώρες", + "minutes": "Λεπτά", + "seconds": "Δευτερόλεπτα", + "advanced": "Προηγμένος" + }, + "timewindow": { + "days": "{ days, plural, 1 { day } other {# days } }", + "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", + "realtime": "Πραγματικός Χρόνος", + "history": "Ιστορικό", + "last-prefix": "Τελευταίος", + "period": "από {{ startTime }} σε {{ endTime }}", + "edit": "Επεξεργασία Χρονικού Πλαισίου", + "date-range": "Εύρος ημερομηνιών", + "last": "Τελευταίος", + "time-period": "Χρονική Περίοδος" + }, + "user": { + "user": "Χρήστης", + "users": "Χρήστες", + "management": "Διαχείριση Χρηστών", + "customer-users": "Χρήστες του Πελάτη", + "tenant-admins": "Διαχειριστές Μισθωτή", + "sys-admin": "Διαχειριστής Συστήματος", + "tenant-admin": "Διαχειριστής Μισθωτή", + "customer": "Πελάτης", + "anonymous": "Ανώνυμος", + "add": "Προσθήκη χρήστη", + "delete": "Διαγραφή χρήστη", + "add-user-text": "Προσθήκη νέου Χρήστη", + "no-users-text": "Δεν βρέθηκαν Χρήστες", + "user-details": "Λεπτομέρειες Χρήστη", + "delete-users": "Διαγραφή Χρηστών", + "delete-user-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το Χρήστη '{{userEmail}}'?", + "delete-user-text": "Προσοχή, μετά την επιβεβαίωση ο Χρήστης και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-users-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 user} other {# users} };", + "delete-users-action-title": "Διαγραφή { count, plural, 1 {1 user} other {# users} }", + "delete-users-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι Χρήστες θα αφαιρεθούν και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "activation-email-sent-message": "Το email ενεργοποίησης στάλθηκε με επιτυχία!", + "resend-activation": "Επανάληψη ενεργοποίησης", + "email": "Email", + "email-required": "Απαιτείται email.", + "invalid-email-format": "Μη έγκυρη μορφή email.", + "first-name": "Όνομα", + "last-name": "Επίθετο", + "description": "Περιγραφή", + "default-dashboard": "Προκαθορισμένος πίνακας ελέγχου", + "always-fullscreen": "Πάντα με πλήρη οθόνη", + "select-user": "Επιλογή Χρήστη", + "no-users-matching": "Δεν βρέθηκαν χρήστες που να ταιριάζουν '{{entity}}'.", + "user-required": "Απαιτείται Χρήστης", + "activation-method": "Μέθοδος ενεργοποίησης", + "display-activation-link": "Εμφάνιση συνδέσμου ενεργοποίησης", + "send-activation-mail": "Αποστολή mail ενεργοποίησης", + "activation-link": "Σύνδεσμος ενεργοποίησης Χρήστη", + "activation-link-text": "΄Προκειμένου να ενεργοποιήσετε το Χρήστη, χρησιμοποιήστε το εξής activation link :", + "copy-activation-link": "Αντιγραφή συνδέσμου ενεργοποίησης", + "activation-link-copied-message": "Ο σύνδεσμος ενεργοποίησης χρήστη έχει αντιγραφεί στο πρόχειρο", + "selected-users": "{ count, plural, 1 {1 user} other {# users} } επιλέχθηκαν", + "search": "Αναζήτηση Χρηστών", + "details": "Λεπτομέρειες", + "login-as-tenant-admin": "Συνδεθείτε ως Διαχειριστής Μισθωτή", + "login-as-customer-user": "Συνδεθείτε ως Χρήστης του Πελάτη", + "select-group-to-add": "Επιλέξτε την ομάδα προορισμού για να προσθέσετε επιλεγμένους Χρήστες", + "select-group-to-move": "Επιλέξτε την ομάδα προορισμού για να μετακινήσετε επιλεγμένους χρήστες", + "remove-users-from-group": "Είστε σίγουροι ότι θέλετε να καταργήσετε { count, plural, 1 {1 user} other {# users} } από την ομάδα '{entityGroup}';", + "group": "Ομάδα από Χρήστες", + "list-of-groups": "{ count, plural, 1 {One user group} other {List of # user groups} }", + "group-name-starts-with": "Ομάδες Χρηστών των οποίων τα ονόματα ξεκινούν με'{{prefix}}'" + }, + "value": { + "type": "Είδος τιμής", + "string": "Συμβολοσειρά", + "string-value": "Τιμή συμβολοσειράς", + "integer": "Ακέραιος", + "integer-value": "Ακέραια τιμή", + "invalid-integer-value": "Μη έγκυρη ακέραια τιμή", + "double": "Πραγματικός", + "double-value": "Πραγματική τιμή", + "boolean": "Λογικό", + "boolean-value": "Λογική τιμή", + "false": "Εσφαλμένος", + "true": "Αληθής", + "long": "Μακρύς" + }, + "widget": { + "widget-library": "Βιβλιοθήκη Widget", + "widget-bundle": "Δέσμη Widget", + "select-widgets-bundle": "Επιλογή δέσμης Widgets", + "management": "Διαχείριση Widget", + "editor": "Συντάκτης Widget", + "widget-type-not-found": "Πρόβλημα φόρτωσης διαμόρφωσης Widget.
Probably associated\n widget type was removed.", + "widget-type-load-error": "Το Widget δεν φορτώθηκε λόγω των παρακάτω σφαλμάτων:", + "remove": "Αφαίρεση Widget", + "edit": "Επεξεργασία Widget", + "remove-widget-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε το Widget '{{widgetTitle}}'?", + "remove-widget-text": "Προσοχή, μετά την επιβεβαίωση το widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "timeseries": "Χρονική σειρά", + "search-data": "Αναζήτηση δεδομένων", + "no-data-found": "Δεν βρέθηκαν δεδομένα", + "latest-values": "Τελευταίες αξίες", + "rpc": "Έλεγχος Widget", + "alarm": "Alarm widget", + "static": "Στατικό widget", + "select-widget-type": "Επιλογή τύπου Widget", + "missing-widget-title-error": "Ο τίτλος Widget πρέπει να καθοριστεί!", + "widget-saved": "Το Widget αποθηκεύτηκε", + "unable-to-save-widget-error": "Δεν είναι δυνατή η αποθήκευση του Widget! Το Widget έχει σφάλματα!", + "save": "Αποθήκευση widget", + "saveAs": "Αποθήκευση widget ως", + "save-widget-type-as": "Αποθήκευση τύπου Widget type ως", + "save-widget-type-as-text": "Παρακαλούμε εισάγετε νέο τίτλο Widget και/ή επιλέξετε στοχευμένη δέσμη widgets", + "toggle-fullscreen": "Λειτουργεία πλήρους οθόνης", + "run": "Εκτέλεση Widget", + "title": "Τίτλος Widget", + "title-required": "Απαιτείται τίτλος Widget.", + "type": "Τύπος Widget", + "resources": "Πόροι", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Αφαίρεση πηγής", + "add-resource": "Προσθήκη πηγής", + "html": "HTML", + "tidy": "Τακτοποιημένος", + "css": "CSS", + "settings-schema": "Ρυθμίσεις σχήματος", + "datakey-settings-schema": "Πλήκτρο δεδομένων σχήματος ρυθμίσεων", + "javascript": "Javascript", + "remove-widget-type-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε τον τύπο Widget '{{widgetName}}'?", + "remove-widget-type-text": "Προσοχή, μετά την επιβεβαίωση ο τύπος Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "remove-widget-type": "Αφαίρεση τύπου Widget", + "add-widget-type": "Προσθήκη νέου τύπου Widget", + "widget-type-load-failed-error": "Αποτυχία φόρτωσης τύπου Widget!", + "widget-template-load-failed-error": "Αποτυχία φόρτωσης προτύπου Widget!", + "add": "Προσθήκη Widget", + "undo": "Αναίρεση αλλαγών Widget", + "export": "Εξαγωγή Widget", + "export-data": "Εξαγωγή δεδομένων Widget", + "export-to-csv": "Εξαγωγή δεδομένων σε CSV...", + "export-to-excel": "Εξαγωγή δεδομένων σε XLS...", + "no-data": "Δεν υπάρχουν δεδομένα για εμφάνιση στο Widget" + }, + "widget-action": { + "header-button": "Κουμπί κεφαλίδας στη νέα κατάσταση του πίνακα ελέγχου", + "update-dashboard-state": "Ενημέρωση της τρέχουσας κατάστασης του dashboard", + "open-dashboard": "Πλοήγηση σε άλλο dashboard", + "custom": "Προσαρμοσμένη ενέργεια", + "target-dashboard-state": "Κατάσταση προορισμού dashboard", + "target-dashboard-state-required": "Απαιτείται κατάσταση προορισμού dashboard", + "set-entity-from-widget": "Ορισμός οντότητας από Widget", + "target-dashboard": "dashboard προορισμού", + "open-right-layout": "Ανοίξτε τη δεξιά διάταξη του dashboard (προβολή κινητού)" + }, + "widgets-bundle": { + "current": "Τρέχουσα δέσμη", + "widgets-bundles": "Δέσμες Widgets", + "add": "Προσθήκη δέσμης Widgets", + "delete": "Διαγραφή δέσμης Widgets", + "title": "Τίτλος", + "title-required": "Απαιτείται τίτλος.", + "add-widgets-bundle-text": "Προσθήκη νέας δέσμης widgets", + "no-widgets-bundles-text": "Δεν βρέθηκαν δέσμες widgets", + "empty": "Η δέσμη Widgets είναι κενή", + "details": "Λεπτομέρειες", + "widgets-bundle-details": "Λεπτομέρειες δέσμης Widgets", + "delete-widgets-bundle-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τη δέσμη Widgets '{{widgetsBundleTitle}}';", + "delete-widgets-bundle-text": "Προσοχή, μετά την επιβεβαίωση η δέσμη Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "delete-widgets-bundles-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, 1 {1 widgets bundle} other {# widgets bundles} };", + "delete-widgets-bundles-action-title": "Διαγραφή { count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text": "Προσοχή, μετά την επιβεβαίωση όλες οι επιλεγμένες δέσμες Widget και όλα τα σχετικά δεδομένα θα διαγραφούν οριστικά.", + "no-widgets-bundles-matching": "Δεν βρέθηκαν δέσμες Widgets που να ταιριάζουν'{{widgetsBundle}}'.", + "widgets-bundle-required": "Απαιτείται δέσμη Widgets.", + "system": "Σύστημα", + "import": "Εισαγωγή δέσμης Widgets", + "export": "Εξαγωγή δέσμης Widgets", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή δέσμης Widgets: {{error}}", + "create-new-widgets-bundle": "Δημιουργία νέας δέσμης Widgets", + "widgets-bundle-file": "Αρχείο δέσμης Widgets", + "invalid-widgets-bundle-file-error": "Δεν είναι δυνατή η εισαγωγή δέσμης Widgets: Μη έγκυρη δομή δεδομένων Widgets." + }, + "widget-config": { + "data": "Δεδομένα", + "settings": "Ρυθμίσεις", + "advanced": "Προηγμένος", + "title": "Τίτλος", + "general-settings": "Γενικές ρυθμίσεις", + "display-title": "Εμφάνιση τίτλου", + "drop-shadow": "Σκίαση", + "enable-fullscreen": "Ενεργοποίηση πλήρους οθόνης", + "enable-data-export": "Ενεργοποίηση εξαγωγής δεδομένων", + "background-color": "Χρώμα φόντου", + "text-color": "Χρώμα κειμένου", + "padding": "Εσωτερικό περιθώριο", + "margin": "Περιθώριο", + "widget-style": "Στυλ Widget", + "title-style": "Στυλ τίτλου", + "mobile-mode-settings": "Ρυθμίσεις λειτουργίας κινητού", + "order": "Εντολή", + "height": "Ύψος", + "units": "Ειδικό σύμβολο για εμφάνιση δίπλα στην αξία", + "decimals": "Αριθμός ψηφίων μετά το κυμαινόμενο σημείο", + "timewindow": "Timewindow", + "use-dashboard-timewindow": "Χρήση dashboard timewindow", + "display-timewindow": "Απεικόνιση timewindow", + "display-legend": "Απεικόνιση λεζάντας", + "datasources": "Πηγές δεδομένων", + "maximum-datasources": "Το μέγιστο { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", + "datasource-type": "Τύπος", + "datasource-parameters": "Παράμετροι", + "remove-datasource": "Κατάργηση της πηγής δεδομένων", + "add-datasource": "Προσθήκη πηγής δεδομένων", + "target-device": "Target device", + "alarm-source": "Πηγή Alarm", + "actions": "Ενέργειες", + "action": "ενέργεια", + "add-action": "Προσθήκη ενέργειας", + "search-actions": "Αναζήτηση ενεργειών", + "action-source": "Πηγή ενέργειας", + "action-source-required": "Απαιτείται πηγή ενέργειας.", + "action-name": "Όνομα", + "action-name-required": "Απαιτείται όνομα ενέργειας.", + "action-name-not-unique": "Μια άλλη ενέργεια με το ίδιο όνομα υπάρχει ήδη.
Το όνομα ενέργειας πρέπει να είναι μοναδικό μέσα στην ίδια πηγή ενέργειας.", + "action-icon": "Εικονίδιο", + "action-type": "Τύπος", + "action-type-required": "Απαιτείται τύπος ενέργειας.", + "edit-action": "Επεξεργασία ενέργειας", + "delete-action": "Διαγραφή ενέργειας", + "delete-action-title": "Διαγραφή ενέργειας Widget", + "delete-action-text": "Είστε σίγουροι ότι θέλετε να διαγράψετε δράση widget με όνομα '{{actionName}}';" + }, + "widget-type": { + "import": "Εισαγωγή τύπου Widget", + "export": "Εξαγωγή τύπου Widget", + "export-failed-error": "Δεν είναι δυνατή η εξαγωγή τύπου Widget: {{error}}", + "create-new-widget-type": "Δημιουργία νέου τύπου Widget", + "widget-type-file": "Αρχείο τύπου Widget", + "invalid-widget-type-file-error": "Δεν είναι δυνατή η εισαγωγή τύπου Widget: Μη έγκυρη δομή δεδομένων τύπου Widget." + }, + "self-registration": { + "self-registration": "Αυτόματη εγγραφή", + "self-registration-url": "Αυτόματη εγγραφή URL", + "captcha-site-key": "reCAPTCHA κλειδί ιστότοπου", + "captcha-secret-key": "reCAPTCHA μυστικό κλειδί", + "notification-email": "Εmail γνωστοποίησης", + "privacy-policy-text": "Κείμενο πολιτικής απορρήτου", + "text-message-page": "Μήνυμα κειμένου για τη σελίδα εγγραφής" + }, + "white-labeling": { + "white-labeling": "Εμφάνιση", + "login-white-labeling": "Εμφάνιση Σύδεσης", + "preview": "Προεπισκόπηση", + "app-title": "Τίτλος εφαρμογής", + "favicon": "Εικονίδιο Ιστότοπου", + "favicon-description": "Εικόνα *.ico, *.gif or *.png με μέγιστο μέγεθος {{kbSize}} KBytes.", + "favicon-size-error": "Η εικόνα ιστότοπου είναι πολύ μεγάλη. Μέγιστο επιτρεπόμενο μέγεθος {{kbSize}} KBytes.", + "favicon-type-error": "Μη έγκυρη μορφή αρχείου εικόνας ιστότοπου. Μόνο εικόνες ICO, GIF ή PNG γίνονται αποδεκτές.", + "drop-favicon-image": "Σύρετε ένα εικονίδιο ιστότοπου ή κάντε κλικ για να επιλέξετε ένα αρχείο για μεταφόρτωση.", + "no-favicon-image": "Δεν έχει επιλεχθεί εικονίδιο", + "logo": "Logo", + "logo-description": "Οποιαδήποτε εικόνα με μέγιστο μέγεθος {{kbSize}} KBytes.", + "logo-size-error": "Η εικόνα του λογότυπου είναι πολύ μεγάλη. Μέγιστο επιτρεπόμενο μέγεθος {{kbSize}} KBytes.", + "logo-type-error": "Μη έγκυρη μορφή αρχείου λογότυπου. Μόνο εικόνες είναι αποδεκτές.", + "drop-logo-image": "Σείρετε μια εικόνα λογότυπου ή κάντε κλικ για να επιλέξετε ένα αρχείο για μεταφόρτωση.", + "no-logo-image": "Δεν έχει επιλεγεί λογότυπο", + "logo-height": "Ύψος λογότυπου, px", + "primary-palette": "Κύρια παλέτα", + "accent-palette": "Παλέτα τονισμών", + "customize-palette": "Προσαρμογή", + "edit-palette": "Επεξεργασία παλέτας", + "save-palette": "Αποθήκευση παλέτας", + "primary-background": "Κύριο χρώμα υποβάθρου", + "secondary-background": "Δευτερεύον χρώμα υποβάθρου", + "hue1": "HUE 1", + "hue2": "HUE 2", + "hue3": "HUE 3", + "page-background-color": "Χρώμα υποβάθρου σελίδας", + "dark-foreground": "Σκοτεινό χρώμα προσκηνίου", + "domain-name": "Όνομα Domain", + "help-link-base-url": "Base url για συνδέσμους βοηθείας", + "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας", + "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: gprs.cloud", + "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας", + "platform-name": "Όνομα πλατφόρμας", + "platform-version": "Έκδοση πλατφόρμας", + "version-mask": "{{name}} v.{{verion}}", + "position": { + "label": "Όνομα πλατφόρμας και θέση έκδοσης", + "under-logo": "Κάτω από το λογότυπο", + "bottom": "Στο κάτω μέρος της φόρμας σύνδεσης" + } + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Κυρ", + "Mon": "Δευ", + "Tue": "Τρι", + "Wed": "Τετ", + "Thu": "Πεμ", + "Fri": "Παρ", + "Sat": "Σαβ", + "Jan": "Ιαν", + "Feb": "Φεβ", + "Mar": "Μαρ", + "Apr": "Απρ", + "May": "Μάιος", + "Jun": "Ιουν", + "Jul": "Ιουλ", + "Aug": "Αυγ", + "Sep": "Σεπ", + "Oct": "Οκτ", + "Nov": "Νοε", + "Dec": "Δεκ", + "January": "Ιανουάριος", + "February": "Φεβρουάριος", + "March": "Μάρτιος", + "April": "Απρίλιος", + "June": "Ιούνιος", + "July": "Ιούλιος", + "August": "Αύγουστος", + "September": "Σεπτέμβριος", + "October": "Οκτώβριος", + "November": "Νοέμβριος", + "December": "Δεκέμβριος", + "Custom Date Range": "Προσαρμοσμένο εύρος ημερομηνιών", + "Date Range Template": "Πρότυπο εύρους ημερομηνιών", + "Today": "Σήμερα", + "Yesterday": "Χθες", + "This Week": "Αυτή την εβοδομάδα", + "Last Week": "Την προηγούμενη εβδομάδα", + "This Month": "Αυτόν τον μήνα", + "Last Month": "Τον προηγούμενο μήνα", + "Year": "Έτος", + "This Year": "Αυτό το χρόνο", + "Last Year": "Τον προηγούμενο χρόνο", + "Date picker": "Επιλογέας ημερομηνίας", + "Hour": "Ώρα", + "Day": "Ημέρα", + "Week": "Εβδομάδα", + "2 weeks": "2 Εβδομάδες", + "Month": "Μήνας", + "3 months": "3 Μήνες", + "6 months": "6 Μήνες", + "Custom interval": "Προσαρμοσμένο διάστημα", + "Interval": "Διάστημα", + "Step size": "Μέγεθος βήματος", + "Ok": "Ok" + } + } + }, + "icon": { + "icon": "Εικονίδιο", + "select-icon": "Επιλογή εικονιδίου ", + "material-icons": "Υλικά εικονίδια", + "show-all": "Προβολή όλων των εικονιδίων" + }, + "subscription": { + "entity-limit-text": "Ωστόσο, μπορείτε να αναβαθμίσετε το πρόγραμμα εγγραφής σας για να αυξήσετε τα όριά σας.", + "upgrade-your-plan": "Αναβάθμιση του σχεδίου συνδρομής", + "white-labeling-feature": "Εμφάνιση χαρακτηριστικού", + "white-labeling-text-full": "Αλλάξτε το διακριτικό τίτλο της πλατφόρμας με το λογότυπο της εταιρείας ή του προϊόντος σας και το συνδυασμό χρωμάτων σε 2 λεπτά.

Αφαιρέστε το \"Powered By\" στο κάτω μέρος του dashboard.
Δεν απαιτείται κώδικας ή επανεκκίνηση υπηρεσίας. Επιτρέψτε και στους πελάτες σας να αλλάξουν το περιβάλλον τους.", + "enable-white-labeling": "Ενεργοποίηση εμφάνισης χαρακτηριστικού τώρα αναβαθμίζοντας το σχέδιο εγγραφής σας!", + "read-more": "Διαβάστε περισσότερα", + "white-labeling-video-text": "Δείτε το εκπαιδευτικό βίντεο παρακάτω για να δείτε πώς λειτουργεί αυτό το χαρακτηριστικό!" + }, + "subscription-error": { + "title": "Παραβίαση συνδρομής", + "warning-title": "Προειδοποίηση συνδρομής", + "limit-reached": { + "device-count": "Έχετε φτάσει τις μέγιστες συσκευές ({{value}}) που επιτρέπονται από το πρόγραμμα εγγραφής σας!", + "asset-count": "Έχετε φτάσει τα μέγιστα assets ({{value}}) που επιτρέπονται από το πρόγραμμα εγγραφής σας!" + }, + "feature-disabled": { + "white-labeling": "Η εμφάνιση χαρακτηριστικού δεν επιτρέπεται από το σχέδιο εγγραφής σας!" + } + }, + "custom": { + "widget-action": { + "action-cell-button": "Κουμπί ενέργειας κελιών", + "row-click": "Κλικ στη σειρά", + "polygon-click": "Κλικ στο πολύγωνο", + "marker-click": "Κλικ στο δείκτη", + "tooltip-tag-action": "Ετικέτα εργαλείου ενέργειας", + "node-selected": "Στον επιλεγμένο κόμβο", + "element-click": "Κλικ στο στοιχείο HTML" + } + }, + "language": { + "language": "Γλώσσα", + "locales": { + "de_DE": "Γερμανικά", + "fr_FR": "Γαλλικά", + "zh_CN": "Κινέζικα", + "en_US": "Αγγλικά", + "it_IT": "Ιταλικά", + "ko_KR": "Κορεάτικα", + "ru_RU": "Ρώσικα", + "es_ES": "Ισπανικά", + "ja_JA": "Ιαπωνικά", + "tr_TR": "Τούρκικα", + "fa_IR": "Περσικά", + "uk_UA": "Ουκρανικά", + "cs_CZ": "Τσέχικα", + "el_GR": "Ελληνικά" + } + } +} \ No newline at end of file diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 006db27e14..3014d9741a 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -271,7 +271,8 @@ "asset-required": "Asset is required", "name-starts-with": "Asset name starts with", "import": "Import assets", - "asset-file": "Asset file" + "asset-file": "Asset file", + "label": "Label" }, "attribute": { "attributes": "Attributes", @@ -822,7 +823,7 @@ "no-keys-found": "No keys found.", "create-new-alias": "Create a new one!", "create-new-key": "Create a new one!", - "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
Entity View aliases must be unique whithin the dashboard.", + "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
Entity View aliases must be unique within the dashboard.", "configure-alias": "Configure '{{alias}}' alias", "no-entity-views-matching": "No entity views matching '{{entity}}' were found.", "alias": "Alias", @@ -845,21 +846,21 @@ "add-entity-view-text": "Add new entity view", "delete": "Delete entity view", "assign-entity-views": "Assign entity views", - "assign-entity-views-text": "Assign { count, plural, 1 {1 entityView} other {# entityViews} } to customer", + "assign-entity-views-text": "Assign { count, plural, 1 {1 entity view} other {# entity views} } to customer", "delete-entity-views": "Delete entity views", "unassign-from-customer": "Unassign from customer", "unassign-entity-views": "Unassign entity views", - "unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entityView} other {# entityViews} } from customer", + "unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entity view} other {# entity views} } from customer", "assign-new-entity-view": "Assign new entity view", "delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?", "delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.", - "delete-entity-views-title": "Are you sure you want to entity view { count, plural, 1 {1 entityView} other {# entityViews} }?", - "delete-entity-views-action-title": "Delete { count, plural, 1 {1 entityView} other {# entityViews} }", + "delete-entity-views-title": "Are you sure you want to delete { count, plural, 1 {1 entity view} other {# entity views} }?", + "delete-entity-views-action-title": "Delete { count, plural, 1 {1 entity view} other {# entity views} }", "delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.", "unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", "unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.", "unassign-entity-view": "Unassign entity view", - "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entityView} other {# entityViews} }?", + "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entity view} other {# entity views} }?", "unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.", "entity-view-type": "Entity View type", "entity-view-type-required": "Entity View type is required.", @@ -1139,6 +1140,7 @@ "column-type": { "name": "Name", "type": "Type", + "label": "Label", "column-type": "Column type", "client-attribute": "Client attribute", "shared-attribute": "Shared attribute", @@ -1195,7 +1197,13 @@ "min": "min", "max": "max", "avg": "avg", - "total": "total" + "total": "total", + "comparison-time-ago": { + "days": "(day ago)", + "weeks": "(week ago)", + "months": "(month ago)", + "years": "(year ago)" + } }, "login": { "login": "Login", @@ -1699,17 +1707,32 @@ }, "input-widgets": { "attribute-not-allowed": "Attribute parameter cannot be used in this widget", + "blocked-location": "Geolocation is blocked in your browser", + "claim-device": "Claim device", + "claim-failed": "Failed to claim the device!", + "claim-not-found": "Device not found!", + "claim-successful": "Device was successfully claimed!", "date": "Date", + "device-name": "Device name", + "device-name-required": "Device name is required", "discard-changes": "Discard changes", "entity-attribute-required": "Entity attribute is required", + "entity-coordinate-required": "Both fields, latitude and longitude, are required", "entity-timeseries-required": "Entity timeseries is required", + "get-location": "Get current location", + "latitude": "Latitude", + "longitude": "Longitude", "not-allowed-entity": "Selected entity cannot have shared attributes", "no-attribute-selected": "No attribute is selected", "no-datakey-selected": "No datakey is selected", + "no-coordinate-specified": "Datakey for latitude/longitude doesn't specified", "no-entity-selected": "No entity selected", "no-image": "No image", + "no-support-geolocation": "Your browser doesn't support geolocation", "no-support-web-camera": "No supported web camera", "no-timeseries-selected": "No timeseries selected", + "secret-key": "Secret key", + "secret-key-required": "Secret key is required", "switch-attribute-value": "Switch entity attribute value", "switch-camera": "Switch camera", "switch-timeseries-value": "Switch entity timeseries value", @@ -1757,7 +1780,8 @@ "tr_TR": "Turkish", "fa_IR": "Persian", "uk_UA": "Ukrainian", - "cs_CZ": "Czech" + "cs_CZ": "Czech", + "el_GR": "Greek" } } } diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json index 80ca275e1f..d3d1c4042f 100644 --- a/ui/src/app/locale/locale.constant-es_ES.json +++ b/ui/src/app/locale/locale.constant-es_ES.json @@ -271,7 +271,8 @@ "asset-required": "El activo es requerido", "name-starts-with": "El nombre del activo comienza con", "import": "Importar activos", - "asset-file": "Archivo del activo" + "asset-file": "Archivo del activo", + "label": "Etiqueta" }, "attribute": { "attributes": "Atributos", @@ -1756,7 +1757,8 @@ "tr_TR": "Turco", "fa_IR": "Persa", "uk_UA": "Ucraniano", - "cs_CZ": "Checo" + "cs_CZ": "Checo", + "el_GR": "Griego" } } } diff --git a/ui/src/app/locale/locale.constant-fa_IR.json b/ui/src/app/locale/locale.constant-fa_IR.json index ee9c011073..34eee792b9 100644 --- a/ui/src/app/locale/locale.constant-fa_IR.json +++ b/ui/src/app/locale/locale.constant-fa_IR.json @@ -246,7 +246,8 @@ "select-asset": "انتخاب دارايي", "no-assets-matching": ".يافت نشد '{{entity}}' هيچ دارايي منطبق بر", "asset-required": "دارايي مود نياز است", - "name-starts-with": "نام دارايي شروع مي شود با" + "name-starts-with": "نام دارايي شروع مي شود با", + "label": "برچسب" }, "attribute": { "attributes": "ويژگي ها", diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json index 8bfefc95f5..b25a532927 100644 --- a/ui/src/app/locale/locale.constant-fr_FR.json +++ b/ui/src/app/locale/locale.constant-fr_FR.json @@ -271,7 +271,8 @@ "unassign-assets-text": "Après la confirmation, tous les actifs sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?", "unassign-from-customer": "Retirer du client", - "view-assets": "Afficher les actifs" + "view-assets": "Afficher les actifs", + "label": "Label" }, "attribute": { "add": "Ajouter un attribut", @@ -1166,7 +1167,8 @@ "tr_TR": "Turc", "fa_IR": "Persane", "uk_UA": "Ukrainien", - "cs_CZ": "Tchèque" + "cs_CZ": "Tchèque", + "el_GR": "Grec" } }, "layout": { diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json index 22ecbfd8b6..cf7529614b 100644 --- a/ui/src/app/locale/locale.constant-it_IT.json +++ b/ui/src/app/locale/locale.constant-it_IT.json @@ -268,7 +268,8 @@ "select-asset": "Seleziona asset", "no-assets-matching": "Nessun asset corrispondente a '{{entity}}' è stato trovato.", "asset-required": "Asset obbligatorio", - "name-starts-with": "Asset con nome che inizia per" + "name-starts-with": "Asset con nome che inizia per", + "label": "Etichetta" }, "attribute": { "attributes": "Attributi", @@ -1713,7 +1714,8 @@ "tr_TR": "Turco", "fa_IR": "Persiana", "uk_UA": "Ucraino", - "cs_CZ": "Ceco" + "cs_CZ": "Ceco", + "el_GR": "Greco" } } } diff --git a/ui/src/app/locale/locale.constant-ja_JA.json b/ui/src/app/locale/locale.constant-ja_JA.json index c79390534e..e1ebd4d6b3 100644 --- a/ui/src/app/locale/locale.constant-ja_JA.json +++ b/ui/src/app/locale/locale.constant-ja_JA.json @@ -236,7 +236,8 @@ "select-asset": "アセットを選択", "no-assets-matching": "'{{entity}}'発見されました。", "asset-required": "資産が必要です", - "name-starts-with": "アセット名はで始まります" + "name-starts-with": "アセット名はで始まります", + "label": "ラベル" }, "attribute": { "attributes": "属性", diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json index d1c29f2a93..5c343d84a5 100644 --- a/ui/src/app/locale/locale.constant-ru_RU.json +++ b/ui/src/app/locale/locale.constant-ru_RU.json @@ -48,7 +48,9 @@ "paste-reference": "Вставить ссылку", "import": "Импортировать", "export": "Экспортировать", - "share-via": "Поделиться в {{provider}}" + "share-via": "Поделиться в {{provider}}", + "continue": "Продолжить", + "discard-changes": "Отменить изменения" }, "aggregation": { "aggregation": "Агрегация", @@ -83,7 +85,28 @@ "timeout-required": "Таймаут обязателен.", "timeout-invalid": "Недействительный таймаут.", "enable-tls": "Включить TLS", - "send-test-mail": "Отправить пробное письмо" + "send-test-mail": "Отправить пробное письмо", + "security-settings": "Настройки безопасности", + "password-policy": "Политика паролей", + "minimum-password-length": "Минимальная длина пароля", + "minimum-password-length-required": "Требуется минимальная длина пароля", + "minimum-password-length-range": "Минимальная длина пароля должна быть в диапазоне от 5 до 50", + "minimum-uppercase-letters": "Минимальное количество прописных букв", + "minimum-uppercase-letters-range": "Минимальное количество прописных букв не может быть отрицательным", + "minimum-lowercase-letters": "Минимальное количество строчных букв", + "minimum-lowercase-letters-range": "Минимальное количество строчных букв не может быть отрицательным", + "minimum-digits": "Минимальное количество цифр", + "minimum-digits-range": "Минимальное количество цифр не может быть отрицательным", + "minimum-special-characters": "Минимальное количество специальных символов", + "minimum-special-characters-range": "Минимальное количество специальных символов не может быть отрицательным", + "password-expiration-period-days": "Срок действия пароля в днях", + "password-expiration-period-days-range": "Срок действия пароля в днях не может быть отрицательным", + "password-reuse-frequency-days": "Частота повторного использования пароля в днях", + "password-reuse-frequency-days-range": "Частота повторного использования пароля в днях не может быть отрицательной", + "general-policy": "Общая политика", + "max-failed-login-attempts": "Максимальное количество неудачных попыток входа в систему, прежде чем учетная запись заблокирована", + "minimum-max-failed-login-attempts-range": "Максимальное количество неудачных попыток входа в систему не может быть отрицательным", + "user-lockout-notification-email": "В случае блокировки учетной записи пользователя отправьте уведомление на электронную почту" }, "alarm": { "alarm": "Оповещение", @@ -150,8 +173,8 @@ "filter-type-single-entity": "Отдельный объект", "filter-type-entity-list": "Список объектов", "filter-type-entity-name": "Название объекта", - "filter-type-state-entity": "Объект, полученный из дашборда", - "filter-type-state-entity-description": "Объект, полученный из параметров дашборда", + "filter-type-state-entity": "Объект из состояния дашборда", + "filter-type-state-entity-description": "Объект, полученный из параметров состояния дашборда", "filter-type-asset-type": "Тип актива", "filter-type-asset-type-description": "Активы типа '{{assetType}}'", "filter-type-asset-type-and-name-description": "Активы типа '{{assetType}}' и названием, начинающимся с '{{prefix}}'", @@ -246,7 +269,10 @@ "select-asset": "Выбрать активы", "no-assets-matching": "Активы, соответствующие '{{entity}}', не найдены.", "asset-required": "Актив обязателен", - "name-starts-with": "Название актива, начинающееся с" + "name-starts-with": "Название актива, начинающееся с", + "import": "Импортировать активы", + "asset-file": "Файл с активами", + "label": "Метка" }, "attribute": { "attributes": "Атрибуты", @@ -303,6 +329,9 @@ "type-relations-delete": "Удалены все отношения", "type-alarm-ack": "Подтвержден", "type-alarm-clear": "Сброшен", + "type-login": "Вход", + "type-logout": "Выход", + "type-lockout": "Заблокирован", "status-success": "Успех", "status-failure": "Сбой", "audit-log-details": "Подробности аудит лога", @@ -665,7 +694,9 @@ "is-gateway": "Гейтвей", "public": "Общедоступный", "device-public": "Устройство общедоступно", - "select-device": "Выбрать устройство" + "select-device": "Выбрать устройство", + "import": "Импортировать устройства", + "device-file": "Файл с устройствами" }, "dialog": { "close": "Закрыть диалог" @@ -771,6 +802,7 @@ "search": "Поиск объектов", "selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }", "entity-name": "Название объекта", + "entity-label": "Метка объекта", "details": "Подробности об объекте", "no-entities-prompt": "Объекты не найдены", "no-data": "Нет данных для отображения", @@ -793,7 +825,7 @@ "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.
В рамках одного дашборда псевдонимы представлений объектов должны быть уникальными.", "configure-alias": "Настроить псевдоним '{{alias}}'", "no-entity-views-matching": "Объекты, соответствующие '{{entity}}', не найдены.", - "alias": "Псевдонимы", + "alias": "Псевдоним", "alias-required": "Псевдоним представления объекта обязателен.", "remove-alias": "Убрать псевдоним представления объекта", "add-alias": "Добавить псевдоним представления объекта", @@ -843,11 +875,12 @@ "events": "События", "details": "Подробности", "copyId": "Копировать ИД представление объекта", - "assignedToCustomer": "Назначенные клиенту", + "assignedToCustomer": "Назначено клиенту", "unable-entity-view-device-alias-title": "Не удалось удалить псевдоним представления объекта", "unable-entity-view-device-alias-text": "Не удалось удалить псевдоним устройства '{{entityViewAlias}}', т.к. он используется следующими виджетами:
{{widgetsList}}", "select-entity-view": "Выбрать представление объекта", "make-public": "Открыть общий доступ к представлению объекта", + "make-private": "Закрыть общий доступ к представлению объекта", "start-date": "Дата начала", "start-ts": "Время начала", "end-date": "Дата окончания", @@ -865,7 +898,11 @@ "attributes-propagation": "Пробросить атрибуты", "attributes-propagation-hint": "Представление объекта автоматически копирует указанные атрибуты с Целевого Объекта каждый раз, когда вы сохраняете или обновляете это представление. В целях производительности атрибуты целевого объекта не пробрасываются в представление объекта на каждом их изменении. Вы можете включить автоматический проброс, настроив в вашей цепочке правило \"copy to view\" и соединив его с сообщениями типа \"Post attributes\" и \"Attributes Updated\".", "timeseries-data": "Данные телеметрии", - "timeseries-data-hint": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения." + "timeseries-data-hint": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения.", + "make-public-entity-view-title": "Вы уверенны, что хотите открыть общий доступ к представленю объекта '{{entityViewName}}'?", + "make-public-entity-view-text": "После подтверждения представление объекта и все связанные с ним данные станут публичными и доступными для других пользователей.", + "make-private-entity-view-title": "Вы уверенны, что хотите закрыть общий доступ к представлению объекта '{{entityViewName}}'?", + "make-private-entity-view-text": "После подтверждения представление объекта и все звязанные с ним данные станут приватными и не будут доступны для других пользователей." }, "event": { "event-type": "Тип события", @@ -1012,6 +1049,7 @@ "modbus-add-server": "Добавить сервер/ведомое устройство", "modbus-add-server-prompt": "Пожалуйста, добавить сервер/ведомое устройство", "modbus-transport": "Транспорт", + "modbus-tcp-reconnect": "Переподключатсься автоматически", "modbus-port-name": "Название последовательного порта", "modbus-encoding": "Кодирование символов", "modbus-parity": "Паритет", @@ -1086,7 +1124,41 @@ }, "import": { "no-file": "Файл не выбран", - "drop-file": "Перетащите JSON файл или кликните для выбора файла." + "drop-file": "Перетащите JSON файл или кликните для выбора файла.", + "drop-file-csv": "Перетащите CSV файл или кликните для выбора файла.", + "column-value": "Значение", + "column-title": "Название", + "column-example": "Пример значений данных", + "column-key": "Ключ атрибута/телеметрии", + "csv-delimiter": "Разделитель в CSV файле", + "csv-first-line-header": "Первая строка содержит названия колонок", + "csv-update-data": "Обновить атрибут/телеметрию", + "import-csv-number-columns-error": "Файл должен содержать как минимум две колонки", + "import-csv-invalid-format-error": "Неверный формат данных. Строка: '{{line}}'", + "column-type": { + "name": "Название", + "type": "Тип", + "label": "Метка", + "column-type": "Тип колонки", + "client-attribute": "Клиентский атрибут", + "shared-attribute": "Общий атрибут", + "server-attribute": "Серверный атрибут", + "timeseries": "Телеметрия", + "entity-field": "Entity field", + "access-token": "Токен" + }, + "stepper-text": { + "select-file": "Выберите файл", + "configuration": "Конфигурация импорта", + "column-type": "Выберите тип колонок", + "creat-entities": "Создание новых объектов", + "done": "Завершено" + }, + "message": { + "create-entities": "{{count}} новый(х) объект(ов) было успешно создано.", + "update-entities": "{{count}} объект(ов) успешно обновлено.", + "error-entities": "Возникла ошибка при создании {{count}} объекта(ов)." + } }, "item": { "selected": "Выбранные" @@ -1123,7 +1195,13 @@ "min": "Мин", "max": "Макс", "avg": "Среднее", - "total": "Сумма" + "total": "Сумма", + "comparison-time-ago": { + "days": "(день назад)", + "weeks": "(неделю назад)", + "months": "(месяц назад)", + "years": "(год назад)" + } }, "login": { "login": "Войти", @@ -1137,6 +1215,7 @@ "remember-me": "Запомнить меня", "forgot-password": "Забыли пароль?", "password-reset": "Пароль сброшен", + "expired-password-reset-message": "Срок действия Вашего пароля закончился! Пожалуйста, создайте новый пароль.", "new-password": "Новый пароль", "new-password-again": "Повторите новый пароль", "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", @@ -1150,6 +1229,7 @@ }, "profile": { "profile": "Профиль", + "last-login-time": "Время последнего входа в систему", "change-password": "Изменить пароль", "current-password": "Текущий пароль" }, @@ -1346,7 +1426,8 @@ "edit": "Изменить временное окно", "date-range": "Диапазон дат", "last": "Последние", - "time-period": "Период времени" + "time-period": "Период времени", + "hide": "Скрыть" }, "user": { "user": "Пользователь", @@ -1389,7 +1470,11 @@ "activation-link-copied-message": "Ссылка для активации пользователя скопировано в буфер обмена", "details": "Подробности", "login-as-tenant-admin": "Войти как администратор владельца", - "login-as-customer-user": "Войти как пользователь клиента" + "login-as-customer-user": "Войти как пользователь клиента", + "disable-account": "Отключить учетную запись пользователя", + "enable-account": "Включить учетную запись пользователя", + "enable-account-message": "Учетная запись пользователя была успешно включена!", + "disable-account-message": "Учетная запись пользователя была успешно отключена!" }, "value": { "type": "Тип значения", @@ -1418,12 +1503,12 @@ "edit": "Редактировать виджет", "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?", "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.", - "timeseries": "Выборка по времени", - "search-data": "Search data", - "no-data-found": "No data found", + "timeseries": "Телеметрия", + "search-data": "Поиск данных", + "no-data-found": "Данные не найдено", "latest-values": "Последние значения", "rpc": "Управляющий виджет", - "alarm": "Alarm widget", + "alarm": "Виджет оповещений", "static": "Статический виджет", "select-widget-type": "Выберите тип виджета", "missing-widget-title-error": "Укажите название виджета!", @@ -1520,6 +1605,7 @@ "decimals": "Количество цифр после запятой", "timewindow": "Временное окно", "use-dashboard-timewindow": "Использовать временное окно дашборда", + "display-timewindow": "Показывать временное окно", "display-legend": "Показать легенду", "datasources": "Источники данных", "maximum-datasources": "Максимальной количество источников данных равно {{count}}", @@ -1544,7 +1630,10 @@ "edit-action": "Редактировать действие", "delete-action": "Удалить действие", "delete-action-title": "Удалить действие виджета", - "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?" + "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?", + "display-icon": "Показывать иконку в названии", + "icon-color": "Цвет иконки", + "icon-size": "Размер иконки" }, "widget-type": { "import": "Импортировать тип виджета", @@ -1614,15 +1703,37 @@ }, "input-widgets": { "attribute-not-allowed": "Атрибут не может быть выбран в этом виджете", + "date": "Дата", + "blocked-location": "Геолокация заблокирована в вашем браузере", + "claim-device": "Подтвердить устройство", + "claim-failed": "Не удалось подтвердить устройство!", + "claim-not-found": "Устройство не найдено!", + "claim-successful": "Устройство успешно подтверждено!", "discard-changes": "Отменить изменения", + "device-name": "Название устройства", + "device-name-required": "Необходимо указать название устройства", "entity-attribute-required": "Значение атрибута обязателено", + "entity-coordinate-required": "Необходимо указать широту и долготу", "entity-timeseries-required": "Значение телеметрии обязательно", + "get-location": "Получить текущее местоположение", + "latitude": "Широта", + "longitude": "Долгота", "not-allowed-entity": "Выбраный объект не имеет общих атрибутов", "no-attribute-selected": "Атрибут не выбран", + "no-datakey-selected": "Ни один datakey не выбран", "no-entity-selected": "Объект не выбран", + "no-coordinate-specified": "Ключ для широты/долготы не указан", + "no-support-geolocation": "Ваш браузер не поддерживает геолокацию", + "no-image": "Нет изображения", + "no-support-web-camera": "Нет поддерживаемой веб-камеры", "no-timeseries-selected": "Параметр телеметрии не выбран", + "secret-key": "Секретный ключ", + "secret-key-required": "Необходимо указать секретный ключ", "switch-attribute-value": "Изменить значение атрибута", + "switch-camera": "Изменить камеру", "switch-timeseries-value": "Изменить значение телеметрии", + "take-photo": "Сделать фото", + "time": "Время", "timeseries-not-allowed": "Телеметрия не может быть выбрана в этом виджете", "update-failed": "Не удалось обновить", "update-successful": "Успешно обновлено", @@ -1639,11 +1750,15 @@ }, "custom": { "widget-action": { - "action-cell-button": "Кнопка действия ячейки", + "action-cell-button": "Кнопка действия в ячейке таблицы", "row-click": "Действий при щелчке на строку", - "marker-click": "Действия при щелчке на указателе", + "marker-click": "Действия при щелчке на маркер", "polygon-click": "Действия при щелчке на полигон", - "tooltip-tag-action": "Действие при подсказке" + "tooltip-tag-action": "Действие при нажатии на ссылку в подсказке", + "node-selected": "Действий при выборе ноды", + "element-click": "Действий при щелчке на HTML элементе", + "pie-slice-click": "Действий при щелчке на секции круговой диаграммы", + "row-double-click": "Действий при двойном щелчке на строку" } }, "language": { @@ -1661,7 +1776,8 @@ "ja_JA": "Японский", "fa_IR": "Персидский", "uk_UA": "Украинский", - "cs_CZ": "Чешский" + "cs_CZ": "Чешский", + "el_GR": "Греческий" } } -} \ No newline at end of file +} diff --git a/ui/src/app/locale/locale.constant-tr_TR.json b/ui/src/app/locale/locale.constant-tr_TR.json index e3726ec841..69d9047dd8 100644 --- a/ui/src/app/locale/locale.constant-tr_TR.json +++ b/ui/src/app/locale/locale.constant-tr_TR.json @@ -236,7 +236,8 @@ "select-asset": "Varlık seç", "no-assets-matching": "'{{entity}}' isimli varlık bulunamadı.", "asset-required": "Varlık gerekli", - "name-starts-with": "... ile başlayan varlık adı" + "name-starts-with": "... ile başlayan varlık adı", + "label": "Etiket" }, "attribute": { "attributes": "Öznitelikler", @@ -1603,7 +1604,8 @@ "tr_TR": "Türkçe", "fa_IR": "Farsça", "uk_UA": "Ukrayna", - "cs_CZ": "Çekçe" + "cs_CZ": "Çekçe", + "el_GR": "Yunanca" } } } \ No newline at end of file diff --git a/ui/src/app/locale/locale.constant-uk_UA.json b/ui/src/app/locale/locale.constant-uk_UA.json index 3cc2b8f1b3..6a07cb575c 100644 --- a/ui/src/app/locale/locale.constant-uk_UA.json +++ b/ui/src/app/locale/locale.constant-uk_UA.json @@ -1,2265 +1,2389 @@ -{ - "access": { - "unauthorized": "Неавторизований", - "unauthorized-access": "Неавторизований доступ", - "unauthorized-access-text": "Щоб отримати доступ до цього ресурсу, потрібно ввійти в систему!", - "access-forbidden": "Доступ заборонено", - "access-forbidden-text": "Недостатньо прав для доступу!
Спробуйте увійти як інший користувач, якщо ви все ще хочете отримати доступ до цього ресурсу.", - "refresh-token-expired": "Дані про сесію застарілі", - "refresh-token-failed": "Не вдається відновити сеанс" - }, - "action": { - "activate": "Активувати", - "suspend": "Призупинити", - "save": "Зберегти", - "saveAs": "Зберегти як", - "cancel": "Скасувати", - "ok": "OK", - "delete": "Видалити", - "add": "Додати", - "yes": "Так", - "no": "Ні", - "update": "Оновити", - "remove": "Видалити", - "search": "Пошук", - "clear-search": "Очистити пошук", - "assign": "Надати", - "unassign": "Позбавити", - "share": "Поділитися", - "make-private": "Зробити приватним", - "apply": "Застосувати", - "apply-changes": "Застосувати зміни", - "edit-mode": "Режим редагування", - "enter-edit-mode": "Ввійти в режим редагування", - "decline-changes": "Відхилити зміни", - "close": "Закрити", - "back": "Назад", - "run": "Запустити", - "sign-in": "Увійти!", - "edit": "Редагувати", - "view": "Переглянути", - "create": "Створити", - "drag": "Перетягнути", - "refresh": "Оновити", - "undo": "Скасувати", - "copy": "Скопіювати", - "paste": "Вставити", - "copy-reference": "Копіювати посилання", - "paste-reference": "Вставити посилання", - "import": "Імпортувати", - "export": "Експортувати", - "share-via": "Поділитися через {{provider}}", - "move": "Перемістити", - "select": "Вибрати" - }, - "aggregation": { - "aggregation": "Агрегація", - "function": "Функція агрегації даних", - "limit": "Максимальні значення", - "group-interval": "Інтервал групування", - "min": "Мінімальний", - "max": "Максимальний", - "avg": "Середній", - "sum": "Сума", - "count": "Рахувати", - "none": "Відсутня" - }, - "admin": { - "general": "Загальне", - "general-settings": "Загальні налаштування", - "outgoing-mail": "Поштовий сервер", - "outgoing-mail-settings": "Налаштування сервера вихідної пошти", - "system-settings": "Налаштування системи", - "test-mail-sent": "Тестовий лист успішно відправлено!", - "base-url": "Базова URL-адреса", - "base-url-required": "Базова URL-адреса обов'язкова.", - "mail-from": "Електронна адреса", - "mail-from-required": "Електронна адреса обов'язкова.", - "smtp-protocol": "Протокол SMTP", - "smtp-host": "Хост SMTP", - "smtp-host-required": "Хост SMTP обов'язковий.", - "smtp-port": "SMTP-порт", - "smtp-port-required": "Ви повинні надати SMTP-порт.", - "smtp-port-invalid": "Це не схоже на дійсний SMTP-порт.", - "timeout-msec": "Час очікування (msec)", - "timeout-required": "Необхідно задати час очікування.", - "timeout-invalid": "Це не схоже на правильний час очікування.", - "enable-tls": "Увімкнути TLS", - "send-test-mail": "Надіслати тестове повідомлення", - "use-system-mail-settings": "Використовувати параметри системного поштового сервера", - "mail-templates": "Шаблони електронної пошти", - "mail-template-settings": "Налаштування шаблонів електронної пошти", - "use-system-mail-template-settings": "Використовувати шаблони системної електронної пошти", - "mail-template": { - "mail-template": "Шаблон електронної пошти", - "test": "Тестове повідомлення", - "activation": "Повідомлення про активацію рахунку", - "account-activated": "Обліковий запис активовано", - "reset-password": "Відновити повідомлення пароля", - "password-was-reset": "Пароль було надіслано повідомленням" - }, - "mail-subject": "Тема повідомлення", - "mail-body": "Вміст повідомлення" - }, - "alarm": { - "alarm": "Сигнал тривоги", - "alarms": "Сигнали тривоги", - "select-alarm": "Вибрати сигнал тривоги", - "no-alarms-matching": "Сигналів тривоги '{{entity}}' не знайдено.", - "alarm-required": "Сигнал тривоги необхідний", - "alarm-status": "Статус сигналу тривоги", - "search-status": { - "ANY": "Будь які", - "ACTIVE": "Активні", - "CLEARED": "Неактивні", - "ACK": "Прийняті", - "UNACK": "Неприйняті" - }, - "display-status": { - "ACTIVE_UNACK": "Активні та неприйняті", - "ACTIVE_ACK": "Активні та прийняті", - "CLEARED_UNACK": "Неактивні та неприйняті", - "CLEARED_ACK": "Неактивні та прийняті" - }, - "no-alarms-prompt": "Сигналів тривоги не знайдено", - "created-time": "Час створення", - "type": "Тип", - "severity": "Серйозність", - "originator": "Ініціатор", - "originator-type": "Тип ініціатору", - "details": "Деталі", - "status": "Статус", - "alarm-details": "Деталі сигналу тривоги", - "start-time": "Початок", - "end-time": "Кінець", - "ack-time": "Час прийняття", - "clear-time": "Час деактивації", - "severity-critical": "Критичні", - "severity-major": "Важливі", - "severity-minor": "Неважливі", - "severity-warning": "Попередження", - "severity-indeterminate": "Невизначені", - "acknowledge": "Прийняти", - "clear": "Деактивувати", - "search": "Шукати сигнали тривоги", - "selected-alarms": "{ count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} } вибрані", - "no-data": "Немає даних для відображення", - "polling-interval": "Інтервал опитування (сек)", - "polling-interval-required": "Необхідно задати інтервал опитування.", - "min-polling-interval-message": "Дозволяється щонайменше 1 секунда інтервалу очікування.", - "aknowledge-alarms-title": "Підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", - "aknowledge-alarms-text": "Ви впевнені, що хочете підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", - "aknowledge-alarm-title": "Підтвердити сигнал тривоги", - "aknowledge-alarm-text": "Ви впевнені, що хочете підтвердити сигнал тривоги?", - "clear-alarms-title": "Деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", - "clear-alarms-text": "Ви впевнені, що хочете деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", - "clear-alarm-title": "Деактивувати сигнал тривоги", - "clear-alarm-text": "Ви впевнені, що хочете деактивувати сигнал тривоги?", - "alarm-status-filter": "Фільтр статусу сигналу тривоги" - }, - "alias": { - "add": "Додати псевдонім ", - "edit": "Редагувати псевдонім", - "name": "Ім'я", - "name-required": "Необхідно вказати псевдонім", - "duplicate-alias": "Псевдонім з такою назвою вже існує.", - "filter-type-single-entity": "Єдина сутність", - "filter-type-entity-group": "Група сутностей", - "filter-type-entity-list": "Список сутностей", - "filter-type-entity-name": "Назва сутності", - "filter-type-entity-group-list": "Список груп сутностей", - "filter-type-entity-group-name": "Назва групи сутностей", - "filter-type-state-entity": "Сутність з стану панелі пристроїв", - "filter-type-state-entity-description": "Сутність, взята з параметрів стану панелі пристроїв", - "filter-type-asset-type": "Тип активу", - "filter-type-asset-type-description": "Тип активів '{{assetType}}'", - "filter-type-asset-type-and-name-description": "Тип активів '{{assetType}}' і ім'я, що починаються з '{{prefix}}'", - "filter-type-device-type": "Тип пристрою", - "filter-type-device-type-description": "Тип пристроїв '{{deviceType}}'", - "filter-type-device-type-and-name-description": "Тип пристроїв '{{deviceType}}' і ім'я, що починаються з '{{prefix}}'", - "filter-type-entity-view-type": "Тип перегляду сутності", - "filter-type-entity-view-type-description": "Перегляд сутності з типом '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Перегляд сутності з типом'{{entityView}}' і іменем, що починаються з '{{prefix}}'", - "filter-type-relations-query": "Запит відносин", - "filter-type-relations-query-description": "{{entities}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Запит пошуку активу", - "filter-type-asset-search-query-description": "Активи з типами {{assetTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Запит пошуку пристрою", - "filter-type-device-search-query-description": "Пристрої з типами {{deviceTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Запит пошуку переглядів сутностей", - "filter-type-entity-view-search-query-description": "Перегляд сутності з типами {{entityViewTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", - "entity-filter": "Фільтр сутності", - "resolve-multiple": "Як декілька сутностей", - "filter-type": "Тип фільтра", - "filter-type-required": "Необхідно вказати тип фільтра.", - "entity-filter-no-entity-matched": "Не знайдено жодних сутностей, які відповідають вказаному фільтру.", - "no-entity-filter-specified": "No entity filter specified", - "root-state-entity": "Use dashboard state entity as root", - "group-state-entity": "Use dashboard state entity as entity group", - "root-entity": "Root entity", - "state-entity-parameter-name": "State entity parameter name", - "default-state-entity": "Default state entity", - "default-state-entity-group": "Default state entity group", - "default-entity-parameter-name": "By default", - "max-relation-level": "Max relation level", - "unlimited-level": "Unlimited level", - "state-entity": "Dashboard state entity", - "entities-of-group-state-entity": "Entities from dashboard state entity group", - "all-entities": "All entities", - "any-relation": "any" - }, - "asset": { - "asset": "Актив", - "assets": "Активи", - "management": "Управління активами", - "view-assets": "Переглянути активи", - "add": "Додати активи", - "assign-to-customer": "Надати клієнту", - "assign-asset-to-customer": "Надати активи клієнту", - "assign-asset-to-customer-text": "Будь ласка, виберіть ресурси, призначені для клієнта", - "no-assets-text": "Не знайдено активів", - "assign-to-customer-text": "Будь ласка, виберіть клієнта, щоб надати активи", - "public": "Публічно", - "assignedToCustomer": "Наданий клієнту", - "make-public": "Зробити актив(и) публічним(и)", - "make-private": "Зробити актив(и) приватним(и)", - "unassign-from-customer": "Позбавити клієнта", - "delete": "Видалити актив", - "asset-public": "Актив є загальнодоступним", - "asset-type": "Тип активу", - "asset-type-required": "Тип активу обов'язковий.", - "select-asset-type": "Виберіть тип активу", - "enter-asset-type": "Введіть тип активу", - "any-asset": "Будь-який актив", - "no-asset-types-matching": "Не знайдено жодних активів, що відповідають даному типу '{{entitySubtype}}'.", - "asset-type-list-empty": "Не вибрано жодного типу активів.", - "asset-types": "Типи активів", - "name": "Ім'я", - "name-required": "Ім'я обов'язкове.", - "description": "Опис", - "type": "Тип", - "type-required": "Тип обов'язковий.", - "details": "Подробиці", - "events": "Події", - "add-asset-text": "Додати новий актив", - "asset-details": "Інформація про актив", - "assign-assets": "Надати активи", - "assign-assets-text": "Надати { count, plural, 1 {1 актив} other {# активи} } клієнту", - "delete-assets": "Видалити активи", - "unassign-assets": "Позбавити активів", - "unassign-assets-action-title": "Позбавити { count, plural, 1 {1 актив} other {# активи} } клієнта", - "assign-new-asset": "Надати новий актив", - "delete-asset-title": "Ви впевнені, що хочете видалити актив '{{assetName}}'?", - "delete-asset-text": "Будьте обережні, після підтвердження, актив і всі пов'язані з ним дані буде втрачено", - "delete-assets-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# активи} }?", - "delete-assets-action-title": "Видалити{ count, plural, 1 {1 актив} other {# активи} }", - "delete-assets-text": "Будьте обережні, після підтвердження всі вибрані об'єкти буде видалено, і всі пов'язані з ними дані буде втрачено.", - "make-public-asset-title": "Ви дійсно хочете, щоб актив '{{assetName}}' був загальнодоступним?", - "make-public-asset-text": "Після підтвердження, актив і всі його дані будуть доступними для інших.", - "make-private-asset-title": "Ви впевнені, що хочете зробити актив {{assetName}} приватним?", - "make-private-asset-text": "Після підтвердження, актив та всі його дані будуть приватними та не будуть доступні іншим.", - "unassign-asset-title": "Ви впевнені, що хочете позбавити активу '{{assetName}}'?", - "unassign-asset-text": "Після підтвердження клієнт буде позбавлений активу. Дані активу не будуть доступні клієнту.", - "unassign-asset": "Позбавити активу", - "unassign-assets-title": "Ви впевнені, що хочете позбавити активів { count, plural, 1 {1 актив} other {# активи} }?", - "unassign-assets-text": "Після підтвердження, клієнт буде позбавлений усіх вибраних активів. Дані активів не будуть доступні клієнту.", - "copyId": "Копіювати Id активу", - "idCopiedMessage": "Id активу був скопійований у буфер обміну", - "select-asset": "Виберіть актив", - "no-assets-matching": "Не знайдено жодних активів, що відповідають'{{entity}}'.", - "asset-required": "Необхідно задати актив", - "name-starts-with": "Назва активу починається з", - "selected-assets": "{ count, plural, 1 {1 актив} other {# активи} } selected", - "search": "Пошук активів", - "select-group-to-add": "Виберіть цільову групу, щоб додати вибрані активи", - "select-group-to-move": "Виберіть цільову групу для переміщення вибраних активів", - "remove-assets-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# актив} } з групи '{entityGroup}'?", - "group": "Група активів", - "list-of-groups": "{ count, plural, 1 {Одна група активів} other {Список # груп активів} }", - "group-name-starts-with": "Групи активів, чиї імена починаються з '{{prefix}}'" - }, - "attribute": { - "attributes": "Атрибути", - "latest-telemetry": "Остання телеметрія", - "attributes-scope": "Область видимості атрибутів", - "scope-latest-telemetry": "Остання телеметрія", - "scope-client": "Клієнтські атрибути", - "scope-server": "Серверні атрибути", - "scope-shared": "Спільні атрибути", - "add": "Додати атрибут", - "add-attribute-prompt": "Будь ласка, додайте атрибут", - "key": "Ключ", - "last-update-time": "Останнє оновлення", - "key-required": "Ключ атрибута обов'язковий.", - "value": "Значення", - "value-required": "Значення атрибута обов'язкове.", - "delete-attributes-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 attribute} other {# attributes} }?", - "delete-attributes-text": "Будьте обережні, після підтвердження, всі виділені атрибути будуть видалені.", - "delete-attributes": "Видалити атрибути", - "enter-attribute-value": "Введіть значення атрибута", - "show-on-widget": "Показати на віджеті", - "widget-mode": "Режим віджетів", - "next-widget": "Наступний віджет", - "prev-widget": "Попередній віджет", - "add-to-dashboard": "Додати до інформаційної панелі", - "add-widget-to-dashboard": "Додати віджет до інформаційної панелі", - "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected ...вибрані вибрати", - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected" - }, - "audit-log": { - "audit": "Операція", - "audit-logs": "Журнал операцій", - "timestamp": "Тимчасова позначка", - "entity-type": "Тип одиниці", - "entity-name": "Назва організації", - "user": "Користувач", - "type": "Тип", - "status": "Статус", - "details": "Подробиці", - "type-added": "Додано", - "type-deleted": "Вилучено", - "type-updated": "Оновлено", - "type-attributes-updated": "Атрибути оновлені", - "type-attributes-deleted": "Атрибути видалені", - "type-rpc-call": "RPC дзвінок", - "type-credentials-updated": "Авторизаційні дані оновлено", - "type-assigned-to-customer": "Призначено клієнту", - "type-unassigned-from-customer": "Позбавлено від клієнта", - "type-activated": "Активовано", - "type-suspended": "Призупинено", - "type-credentials-read": "Авторизаційні дані прочитані", - "type-attributes-read": "Атрибути читаються", - "type-added-to-entity-group": "Додано до групи", - "type-removed-from-entity-group": "Вилучено з групи", - "type-relation-add-or-update": "Відношення оновлено", - "type-relation-delete": "Відношення видалено", - "type-relations-delete": "Всі відношення видалено", - "type-alarm-ack": "Визнано", - "type-alarm-clear": "Очищено", - "type-rest-api-rule-engine-call": "Rule engine REST API call", - "status-success": "Успішно", - "status-failure": "Невдало", - "audit-log-details": "Подробиці журналу операцій", - "no-audit-logs-prompt": "Жодних журналів операцій не знайдено", - "action-data": "Дані про дії", - "failure-details": "Невдалі подробиці", - "search": "Пошук журналів перевірки", - "clear-search": "Очистити пошук" - }, - "confirm-on-exit": { - "message": "У вас є незбережені зміни. Ви впевнені, що хочете залишити цю сторінку?", - "html-message": "У вас є незбережені зміни.
Ви впевнені, що хочете залишити цю сторінку?", - "title": "Незбережені зміни" - }, - "contact": { - "country": "Країна", - "city": "Місто", - "state": "Штат / Провінція", - "postal-code": "Поштовий індекс", - "postal-code-invalid": "Неправильний формат поштового індексу.", - "address": "Адреса", - "address2": "Адреса 2", - "phone": "Телефон", - "email": "Електронна пошта", - "no-address": "Немає адреси" - }, - "common": { - "username": "Ім'я користувача", - "password": "Пароль", - "enter-username": "Введіть ім'я користувача", - "enter-password": "Введіть пароль", - "enter-search": "Введіть пошук" - }, - "converter": { - "converter": "Перетворювач даних", - "converters": "Перетворювачі даних", - "select-converter": "Виберіть перетворювач даних", - "no-converters-matching": "Не має перетворювачів даних, які відповідають '{{entity}}'.", - "converter-required": "Необхідно вказати перетворювач даних", - "delete": "Видалити перетворювач даних", - "management": "Управління перетворювачами даних", - "add-converter-text": "Додати новий перетворювач даних", - "no-converters-text": "Перетворювачів даних не знайдено", - "selected-converters": "{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} } вибраний", - "delete-converter-title": "Ви впевнені, що хочете видалити перетворювач даних '{{converterName}}'?", - "delete-converter-text": "Будьте обережні, після підтвердження, перетворювач даних та всі пов'язані з ним дані,стануть недоступними).", - "delete-converters-title": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }?", - "delete-converters-action-title": "Видалити { count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }", - "delete-converters-text": "Будьте обережні, після підтвердження всі вибрані перетворювачі даних буде видалено, і всі пов'язані з ними дані буде втрачено.", - "events": "Події", - "add": "Додати перетворювач даних", - "converter-details": "Подробиці про перетворювач даних", - "details": "Подробиці", - "copyId": "Копіювати Id перетворювача даних", - "idCopiedMessage": "Id перетворювача даних було скопійовано у буфер обміну", - "debug-mode": "Режим налагодження", - "name": "Ім'я", - "name-required": "Ім'я обов'язкове.", - "description": "Опис", - "decoder": "Декодер", - "encoder": "Кодер", - "test-decoder-fuction": "Тестування функції декодера", - "test-encoder-fuction": "Тестування функції кодера", - "decoder-input-params": "Параметри введення декодера", - "encoder-input-params": "Параметри введення кодера", - "payload": "Вхідне повідомлення", - "payload-content-type": "Тип контенту вхідного повідомлення", - "payload-content": "Зміст вхідного повідомлення", - "message": "Повідомлення", - "message-type": "Тип повідомлення", - "message-type-required": "Необхідно задати тип повідомлення", - "test": "Тест", - "metadata": "Метадані", - "metadata-required": "Записи метаданих не можуть бути порожніми.", - "integration-metadata": "Інтеграція метаданих", - "integration-metadata-required": "Записи інтеграції метаданих не можуть бути порожніми.", - "output": "Вихідні дані", - "import": "Імпорт перетворювача даних", - "export": "Експорт перетворювача даних", - "export-failed-error": "Неможливо експортувати перетворювач даних: {{помилка}}", - "create-new-converter": "Створити новий перетворювач даних", - "converter-file": "Файл перетворювача даних(конвектер файл)", - "invalid-converter-file-error": "Неможливо імпортувати перетворювач даних: недійсна структура даних перетворювача.", - "type": "Тип", - "type-required": "Необхідно задати тип.", - "type-uplink": "Від пристрою", - "type-downlink": "До пристрою" - }, - "content-type": { - "json": "Json", - "text": "Текст", - "binary": "Бінарний (Base64)" - }, - "customer": { - "customer": "Клієнт", - "customers": "Клієнти", - "management": "Клієнтський менеджмент", - "dashboard": "Інформаційна панель клієнта", - "dashboards": "Інформаційні панелі клієнта", - "devices": "Пристрої клієнта", - "entity-views": "Представлення сутностей", - "assets": "Клієнтські активи", - "public-dashboards": "Публічні інформаційні панелі", - "public-devices": "Публічні пристрої", - "public-assets": "Публічні активи", - "public-entity-views": "Публічне представлення сутностей 440", - "add": "Додати клієнта", - "delete": "Видалити клієнта", - "manage-customer-users": "Керування користувачами клієнта", - "manage-customer-devices": "Керування пристроями клієнта", - "manage-customer-dashboards": "Керування інформаційними панелями клієнта", - "manage-public-devices": "Керувати загальнодоступними пристроями", - "manage-public-dashboards": "Керування загальнодоступними інформаційними панелями", - "manage-customer-assets": "Керування активами клієнта", - "manage-public-assets": "Керування загальнодоступними активами", - "add-customer-text": "Додати нового клієнта", - "no-customers-text": "Клієнтів не знайдено", - "customer-details": "Інформація про клієнта", - "delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?", - "delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.", - "delete-customers-title": "Ви впевнені, що хочете видалити {count, plural, 1 {1 клієнт}, інші {# клієнти}}?", - "delete-customers-action-title": "Видалити{ count, plural, 1 {1 клієнт} other {# клієнти} }", - "delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.", - "manage-users": "Керування користувачами", - "manage-assets": "Керування активами", - "manage-devices": "Керування пристроями", - "manage-dashboards": "Керування інформаційними панелями", - "title": "Назва", - "title-required": "Необхідно задати назву.", - "description": "Опис", - "details": "Подробиці", - "events": "Події", - "copyId": "Копіювати Id клієнта", - "idCopiedMessage": "Id клієнта було скопійовано в буфер обміну", - "select-customer": "Виберіть клієнта", - "no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.", - "customer-required": "Необхідно задати клієнта", - "selected-customers": "{ count, plural, 1 {1 клієнт} інші {# клієнти} } вибрано", - "search": "Пошук клієнтів", - "select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів", - "select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів", - "remove-customers-from-group": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 клієнт} other {# клієнти} } з групи'{entityGroup}'?", - "group": "Група клієнтів", - "list-of-groups": "{ count, plural, 1 {Одна група клієнтів} other {Список # груп клієнтів} }", - "group-name-starts-with": "Групи клієнтів, імена яких починаються з '{{prefix}}'", - "select-default-customer": "Виберати клієнта за замовчуванням", - "default-customer": "Клієнт за замовчуванням", - "default-customer-required": "Необхідно вказати клієнта за замовчуванням для налагодження панелі візуалізації на рівні замовника", - "allow-white-labeling": "Дозволити брендування" - }, - "custom-translation": { - "custom-translation": "Переклад для користувача", - "translation-map": "Карта перекладу", - "key": "Ключ перекладу", - "import": "Імпорт перекладу", - "export": "Експорт перекладу", - "export-data": "Дані про експорт перекладу", - "import-data": "Дані про імпорт перекладу", - "translation-file": "Файл перекладу", - "invalid-translation-file-error": "Неможливо імпортувати файл перекладу: недійсна структура даних перекладу.", - "custom-translation-hint": "Визначте індивідуальний переклад в JSON нижче. Цей JSON перезапише переклад за замовчуванням. Натисніть 'Завантажити файл перекладу', щоб отримати існуючий переклад. Ви також можете скористатись завантаженим файлом як посиланням на наявні пари параметрів перекладу ключ-значення.", - "download-locale-file": "Завантажити файл перекладу" - }, - "datetime": { - "date-from": "Дата від", - "time-from": "Час від", - "date-to": "Дата до", - "time-to": "Час до" - }, - "dashboard": { - "dashboard": "Панель приладів", - "dashboards": "Панелі приладів", - "management": "Управління панеллю приладів", - "view-dashboards": "Переглянути панелі приладів", - "add": "Додати панель приладів", - "assign-dashboard-to-customer": "Призначити панель(і) приладів замовнику", - "assign-dashboard-to-customer-text": "Будь ласка, виберіть панелі пристроїв, щоб призначити їх клієнту", - "assign-to-customer-text": "Виберіть клієнта, щоб призначити панелі пристроїв", - "assign-to-customer": "Призначити клієнту", - "unassign-from-customer": "Позбавити клієнта", - "make-public": "Зробити панель приладів публічною", - "make-private": "Зробити панель приладів приватною", - "manage-assigned-customers": "Керування призначеними клієнтами", - "assigned-customers": "Призначені клієнтам", - "assign-to-customers": "Призначити панелі приладів клієнтам", - "assign-to-customers-text": "Виберіть клієнтів для призначення панелей приладів", - "unassign-from-customers": "Позбавити клієнтів призначенних панелей приладів", - "unassign-from-customers-text": "Виберіть клієнтів для позбавлення їх призначених панелей приладів", - "no-dashboards-text": "Панелі приладів не знайдені", - "no-widgets": "Не налаштовано жодних віджетів", - "add-widget": "Додати новий віджет", - "title": "Назва", - "select-widget-title": "Вибрати віджет", - "select-widget-subtitle": "Список доступних типів віджетів", - "delete": "Видалити панель приладів", - "title-required": "Необхідно задати назву.", - "description": "Опис", - "details": "подробиці", - "dashboard-details": "Подробиці панелі приладів", - "add-dashboard-text": "Додати нову панель приладів", - "assign-dashboards": "Призначити панель приладів", - "assign-new-dashboard": "Призначити нову панель приладів", - "assign-dashboards-text": "Призначити { count, plural, 1 {1 панель приладів} other {# панелі приладів} } користувачам", - "unassign-dashboards-action-text": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", - "delete-dashboards": "Видалити панель приладів", - "unassign-dashboards": "Позбавити панелей приладів", - "unassign-dashboards-action-title": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", - "delete-dashboard-title": "Ви впевнені, що хочете видалити панель приладів '{{назва панелі приладів}}'?", - "delete-dashboard-text": "Будьте обережні, після підтвердження, панель приладів і всі пов'язані з нею дані стануть недоступними.", - "delete-dashboards-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }?", - "delete-dashboards-action-title": "Видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }", - "delete-dashboards-text": "Будьте обережні, після підтвердження, всі вибрані панелі приладів буде видалено, і всі пов'язані з ними дані стануть недоступними.", - "unassign-dashboard-title": "Ви впевнені, що хочете позбавити панелі приладів '{{назва інформаційної панелі}}'?", - "unassign-dashboard-text": "Після підтвердження, клієнт буде позбавлений панелі приладів. Панель приладів і пов'язані з нею дані будуть недоступні клієнтові.", - "unassign-dashboard": "Позбавити панелі приладів", - "unassign-dashboards-title": "Ви впевнені, що хочете позбавити { count, plural, 1 {1 панелі приладів} other {# панелей приладів} }?", - "unassign-dashboards-text": "Після підтвердження, клієтн буде позбавлений усіх вибраних панелей приладів і даних, які з ними пов'язані.", - "public-dashboard-title": "Панель приладів тепер публічна", - "public-dashboard-text": "Ваша панель приладів {{dashboardTitle}} тепер публічна і доступна іншим link:", - "public-dashboard-notice": "Note: Не забудьте зробити спільні пристрої загальнодоступними, щоб отримати доступ до їхніх даних.", - "make-private-dashboard-title": "Ви впевнені, що хочете зробити панель приладів '{{назва панелі приладів}}' приватною?", - "make-private-dashboard-text": "Після підтвердження панель приладів стане приватною і не буде доступною іншим.", - "make-private-dashboard": "Зробити панель приладів приватною", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Вибрати панель приладів", - "no-dashboards-matching": "Не знайдено жодних панелей прилодів'{{entity}}' які відповідають.", - "dashboard-required": "Необхідно задати панель приладів.", - "select-existing": "Виберіть існуючу панель приладів", - "create-new": "Створити нову панель приладів", - "new-dashboard-title": "Нова назва панелі приладів", - "open-dashboard": "Відрити панель приладів", - "set-background": "Встановити фон", - "background-color": "Колір фону", - "background-image": "Фонове зображення", - "background-size-mode": "Режим фонового розміру", - "no-image": "Не вибрано жодного зображення", - "drop-image": "Перетягніть зображення або клацніть, щоб вибрати файл для завантаження.", - "settings": "Налаштування", - "columns-count": "Кількість стовпців", - "columns-count-required": "Необхідно вказати кількість стовпців.", - "min-columns-count-message": "Дозволений мінімум -10 стовпців.", - "max-columns-count-message": "Дозволений максимум - 1000 стовпців.", - "widgets-margins": "Відступ між віджетами", - "horizontal-margin": "Горизонтальний відступ", - "horizontal-margin-required": "Необхідно вказати горизонтальний відступ.", - "min-horizontal-margin-message": "Допустиме мінімальне значення горизонтального відступу - 0.", - "max-horizontal-margin-message": "Допустиме максимальне значення горизонтального відступу - 50.", - "vertical-margin": "Вертикальний відступ", - "vertical-margin-required": "Необхідно вказати вертикальний відступ.", - "min-vertical-margin-message": "Допустиме мінімальне значення вертикального відступу - 0.", - "max-vertical-margin-message": "Допустиме максимальне значення вертикального відступу - 50.", - "autofill-height": "Висота автоматичного заповнення макета", - "mobile-layout": "Налаштування макета для мобільних пристроїв", - "mobile-row-height": "Висота рядка для мобільних пристроїв, px", - "mobile-row-height-required": "Потрібно вказати значення висоти рядка для мобільних пристроїв.", - "min-mobile-row-height-message": "Допустиме мінімальне значення висоти рядка для мобільних пристроїв - 5 пікселів.", - "max-mobile-row-height-message": "Допустиме максимальне значення висоти рядка для мобільних пристроїв - 200 пікселів.", - "display-title": "Відображати назву панелі візуалізації", - "toolbar-always-open": "Тримайте панель візуалізації відкритою", - "title-color": "Колір назви", - "display-dashboards-selection": "Відображення вибору панелей візуалізації", - "display-entities-selection": "Вибір відображення сутності", - "display-dashboard-timewindow": "Відобразити налаштування часового проміжку", - "display-dashboard-export": "Відображення експорту", - "import": "Імпортувати панель візуалізації", - "export": "Експортувати панель візуалізації", - "export-failed-error": "Неможливо експортувати панель візуалізації: {{error}}", - "export-pdf": "Експортувати як PDF", - "export-png": "Експортувати як PNG", - "export-jpg": "Експортувати як JPEG", - "export-json-config": "Експортувати конфігурацію JSON", - "download-dashboard-progress": "Генерування панелі візуалізації {{reportType}} ...", - "create-new-dashboard": "Створити нову панель візуалізації", - "dashboard-file": "Файл панелі візуалізації", - "invalid-dashboard-file-error": "Неможливо імпортувати панель візуалізації: неправильна структура даних панелі візуалізації.", - "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard Налаштування псевдонімів, що використовуються імпортованою панеллю візуалізації", - "create-new-widget": "Створити новий віджет", - "import-widget": "Імпортувати віджет", - "widget-file": "Файл віджета", - "invalid-widget-file-error": "Неможливо імпортувати віджет: неправильна структура даних віджета.", - "widget-import-missing-aliases-title": "Налаштувати псевдоніми, що використовуються імпортованим віджетом", - "open-toolbar": "Відкрити панель інструменів ", - "close-toolbar": "Закрити панель інструменів", - "configuration-error": "Помилка конфігурації", - "alias-resolution-error-title": "Помилка конфігурації псевдонімів панелі візуалізації", - "invalid-aliases-config": "Неможливо знайти пристрої, які відповідають певному фільтру псевдонімів.
Зверніться до свого адміністратора, щоб вирішити цю проблему.", - "select-devices": "Вибрати пристрої", - "assignedToCustomer": "Призначений клієнту", - "assignedToCustomers": "Призначений клієнтам", - "public": "Публічно", - "public-link": "Публічне посилання", - "copy-public-link": "Копіювати публічне посилання", - "public-link-copied-message": "Публічне посилання було скопійоване в буфер обміну панелі візуалізації", - "manage-states": "Керування станами панелі візуалізації", - "states": "Стани панелі візуалізації", - "search-states": "Пошук станів панелі візуалізації", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } вибрано", - "edit-state": "Редагувати стан панелі візуалізації", - "delete-state": "Видалити стан панелі візуалізації ", - "add-state": "Додати стан панелі візуалізації", - "state": "Стан панелі візуалізації", - "state-name": "Ім'я", - "state-name-required": "Необхідно вказати назву стану панелі візуалізації.", - "state-id": "Стан Id", - "state-id-required": "Необхідно вказати id стану панелі візуалізації.", - "state-id-exists": "Стан інформаційної панелі з таким id вже існує.", - "is-root-state": "Основний стан", - "delete-state-title": "Видалити стан панелі візуалізації", - "delete-state-text": "Ви впевнені, що хочете видалити стан панелі візуалізації з іменем '{{stateName}}'?", - "show-details": "Показати деталі", - "hide-details": "Приховати деталі", - "select-state": "Виберіть цільовий стан", - "state-controller": "Контроль стану" - }, - "datakey": { - "settings": "Налаштування", - "advanced": "Додатково", - "label": "Мітка", - "color": "Колір", - "units": "Спеціальний символ, який показує наступне значення", - "decimals": "Кількість цифр після плаваючої точки", - "data-generation-func": "Функція генерації даних", - "use-data-post-processing-func": "Використовувати функцію пост-обробки даних", - "configuration": "Конфігурація ключа даних", - "timeseries": "Телеметрія", - "attributes": "Атрибути", - "alarm": "Поля сигнала тривоги", - "timeseries-required": "Необхідно вказати Телеметрія.", - "timeseries-or-attributes-required": "Необхідно вказати телеметрію/атрибути.", - "maximum-timeseries-or-attributes": "Максимальні { count, plural, 1 {1 телеметрія/атрибут дозволені.} other {# телеметрія/атрибути дозволені} }", - "alarm-fields-required": "Необхідно вказати поля сигнала тривоги.", - "function-types": "Типи функцій", - "function-types-required": "Необхідно вказати типи функцій.", - "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", - "time-description": "мітка часу поточного значення;", - "value-description": "поточне значення;", - "prev-value-description": "результат попереднього виклику функції;", - "time-prev-description": "мітка часу попереднього значення;", - "prev-orig-value-description": "оригінальне попереднє значення;" - }, - "datasource": { - "type": "Тип джерела даних", - "name": "Ім'я", - "add-datasource-prompt": "Додайте джерело даних" - }, - "details": { - "details": "Деталі", - "edit-mode": "Режим редагування", - "toggle-edit-mode": "Перемкнути режим редагування" - }, - "device": { - "device": "Пристрій", - "device-required": "Необхідно задати пристрій.", - "devices": "Пристрої", - "management": "Управління пристроєм", - "view-devices": "Перегляд пристроїв", - "device-alias": "Псевдонім пристрою", - "aliases": "Псевдонім пристроїв", - "no-alias-matching": "'{{alias}}' не знайдено.", - "no-aliases-found": "Псевдонімів не знайдено.", - "no-key-matching": "'{{key}}' не знайдено.", - "no-keys-found": "Ключі не знайдено.", - "create-new-alias": "Створити новий!", - "create-new-key": "Створити новий!", - "duplicate-alias-error": "Псевдонім з таким іменем '{{alias}}' вже існює.
Псевдоніми пристроїв повинні бути унікальними на панелі візуалізації.", - "configure-alias": "Налаштувати псевдонім '{{alias}}'", - "no-devices-matching": "Не знайдено жодних пристроїв, які відповідають '{{entity}}'.", - "alias": "Псевдонім", - "alias-required": "Необхідно задати псевдонім пристрою.", - "remove-alias": "Видалити псевдонім пристрою", - "add-alias": "Додати псевдонім пристрою", - "name-starts-with": "Ім'я пристрою починається з", - "device-list": "Список пристроїв", - "use-device-name-filter": "Використати фільтр", - "device-list-empty": "Не вибрано жодного пристрою.", - "device-name-filter-required": "Необхідно задати назву фільтра пристрою.", - "device-name-filter-no-device-matched": "Не знайдено жодних пристроїв, що починаються з '{{device}}'.", - "add": "Додати пристрій", - "assign-to-customer": "Призначити клієнту", - "assign-device-to-customer": "Призначити пристрій (ої) клієнту", - "assign-device-to-customer-text": "Виберіть пристрої, які слід призначити клієнту", - "make-public": "Зробити пристрій публічним", - "make-private": "Зробити пристрій приватним", - "no-devices-text": "Не знайдено жодного пристрою", - "assign-to-customer-text": "Виберіть клієнта для призначення пристрою (їв)", - "device-details": "Деталі пристрою", - "add-device-text": "Додати новий пристрій", - "credentials": "Авторизаційні дані", - "manage-credentials": "Керування авторизаційними даними", - "delete": "Видалити пристрій", - "assign-devices": "Призначити пристрої", - "assign-devices-text": "Призначити { count, plural, 1 {1 пристрій} other {# пристрої} } клієнту", - "delete-devices": "Видалити пристрої", - "unassign-from-customer": "Позбавити клієнта пристроїв", - "unassign-devices": "Позбавити пристроїв", - "unassign-devices-action-title": "Позбавити клієнта { count, plural, 1 {1 пристрою} other {# пристроїв} }", - "assign-new-device": "Призначити новий пристрій", - "make-public-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' публічним?", - "make-public-device-text": "Після підтвердження пристрій і всі його дані будуть публічними та доступними для інших.", - "make-private-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' приватним?", - "make-private-device-text": "Після підтвердження пристрій і всі його дані будуть приватними та недоступними для інших.", - "view-credentials": "Переглянути авторизаційні дані", - "delete-device-title": "Ви впевнені, що хочете видалити пристрій '{{deviceName}}'?", - "delete-device-text": "Будьте обережні, після підтвердження, пристрій і всі пов'язані з ним дані стануть недоступними.", - "delete-devices-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} }?", - "delete-devices-action-title": "Видалити { count, plural, 1 {1 пристрій} other {# пристрої} }", - "delete-devices-text": "Будьте обережні, після підтвердження, всі вибрані пристрої будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "unassign-device-title": "Ви впевнені, що хочете позбавити пристрою '{{deviceName}}'?", - "unassign-device-text": "Після підтвердження, клієнт буде позбавлений пристрою.", - "unassign-device": "Позбавити пристою", - "unassign-devices-title": "Ви впевненені, що хочете позбавити { count, plural, 1 {1 пристрою} other {# пристроїв} }?", - "unassign-devices-text": "Після підтвердження, клієнт буде позбавлений пристрою і пристрій стане не доступним клієнту", - "device-credentials": "Авторизаційні дані прстрою", - "credentials-type": "Тип авторизаційних даних", - "access-token": "Маркер доступу", - "access-token-required": "Необхідно вказати маркер доступу.", - "access-token-invalid": "Маркер доступу має бути від 1 до 20 символів.", - "rsa-key": "Публічний ключ RSA", - "rsa-key-required": "Необхідно вказати публічний ключ RSA.", - "secret": "Секрет", - "secret-required": "Необхідно вказати секрет.", - "device-type": "Тип пристрою", - "device-type-required": "Необхідно вказати тип пристрою.", - "select-device-type": "Виберіть тип пристрою", - "enter-device-type": "Введіть тип пристрою", - "any-device": "Будь-який пристрій", - "no-device-types-matching": "Не знайдено типів пристроїв, які відповідають '{{entitySubtype}}'.", - "device-type-list-empty": "Не вибрано типів пристроїв.", - "device-types": "Типи пристрою", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "events": "Події", - "details": "Деталі", - "copyId": "Копіювати Id пристрою", - "copyAccessToken": "Копіювати маркер доступу", - "idCopiedMessage": "Id пристрою скопійовано в буфер обміну", - "accessTokenCopiedMessage": "Маркер доступу до пристрою скопійовано в буфер обміну", - "assignedToCustomer": "Призначений клієнту", - "unable-delete-device-alias-title": "Неможливо видалити псевдонім пристрою", - "unable-delete-device-alias-text": "Псевдонім пристрою '{{deviceAlias}}' не може бути видалений, оскільки він використовується наступним(и) віджетом(ами):
{{widgetsList}}", - "is-gateway": "Шлюз", - "public": "Публічно", - "device-public": "Пристрій є публічним", - "select-device": "Виберіть пристрій", - "selected-devices": "{ count, plural, 1 {1 пристрій} other {# пристрої} } вибрано", - "search": "Шукати пристрої", - "select-group-to-add": "Виберіть цільову групу, щоб додати вибраний пристрій", - "select-group-to-move": "Виберіть цільову групу для переміщення вибраних пристроїв", - "remove-devices-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} } з групи '{entityGroup}'?", - "group": "Група пристроїв", - "list-of-groups": "{ count, plural, 1 {Одна група пристроїв} other {Список # груп пристроїв} }", - "group-name-starts-with": "Групи пристроїв, імена яких починаються з '{{prefix}}'" - }, - "dialog": { - "close": "Закрити діалогове вікно" - }, - "direction": { - "column": "Колонка", - "row": "Рядок" - }, - "error": { - "unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.", - "unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}", - "unknown-error": "Невідома помилка" - }, - "entity": { - "entity": "Сутність", - "entities": "Сутності", - "aliases": "Псевдоніми сутності", - "entity-alias": "Псевдонім сутності", - "unable-delete-entity-alias-title": "Неможливо видалити псевдонім сутності", - "unable-delete-entity-alias-text": "Псевдонім сутності'{{entityAlias}}' неможливо видалити, так як це використовується наступним віджетом(s):
{{widgetsList}}", - "duplicate-alias-error": "Знайдено повторюваний псевдонім '{{alias}}'.
Псевдоніми сутностей повинні бути унікальними на панелі візуалізації.", - "missing-entity-filter-error": "Відсутній фільтр для псевдоніма '{{alias}}'.", - "configure-alias": "Налаштувати '{{alias}}' псевдонім", - "alias": "Псевдонім", - "alias-required": "Необхідно вказати псевдонім сутності.", - "remove-alias": "Видалити псевдонім сутності", - "add-alias": "Додати псевдонім сутності", - "entity-list": "Список сутності", - "entity-type": "Тип сутності", - "entity-types": "Типи сутності", - "entity-type-list": "Список типу сутності", - "any-entity": "Будь-яка сутність", - "enter-entity-type": "Введіть тип сутності", - "no-entities-matching": "Не знайдено жожних сутностей, що відповідають '{{entity}}' що відповідають.", - "no-entity-types-matching": "Не знайдено жожних типів сутностей, що відповідають '{{entityType}}'.", - "name-starts-with": "Назва починається з", - "use-entity-name-filter": "Використовуйте фільтр", - "entity-list-empty": "Не вибрано жодних сутностей.", - "entity-type-list-empty": "Не вибрано жодних типів сутностей.", - "entity-name-filter-required": "Необхідно задати фільтр по імені.", - "entity-name-filter-no-entity-matched": "Не знайдено жодних сутностей, що починаються з '{{entity}}'.", - "all-subtypes": "Всі", - "select-entities": "Виберіть сутність", - "no-aliases-found": "Псевдонімів не знайдено.", - "no-alias-matching": "'{{alias}}' не знайдено.", - "create-new-alias": "Створити новий псевдонім!", - "key": "Ключ", - "key-name": "Ім'я ключа", - "no-keys-found": "No keys found.", - "no-key-matching": "'{{key}}' не знайдено.", - "create-new-key": "Створити новий ключ!", - "type": "Тип", - "type-required": "Необхідно задати тип сутності.", - "type-device": "Пристрій", - "type-devices": "Пристрої", - "list-of-devices": "{ count, plural, 1 {Один пристрій} other {Список # пристроїв} }", - "device-name-starts-with": "Пристрої, імена яких починаються з '{{prefix}}'", - "type-asset": "Актив", - "type-assets": "Активи", - "list-of-assets": "{ count, plural, 1 {Один актив} other {Список # активів} }", - "asset-name-starts-with": "Активи, імена яких починаються з '{{prefix}}'", - "type-entity-view": "Перегляд сутності", - "type-entity-views": "Перегляди сутності", - "list-of-entity-views": "{ count, plural, 1 {Один перегляд сутності} other {Список # переглядів сутності} }", - "entity-view-name-starts-with": "Перегляди сутностей, імена яких починаються з '{{prefix}}'", - "type-rule": "Правило", - "type-rules": "Правила", - "list-of-rules": "{ count, plural, 1 {Одне правило} other {Список # правил} }", - "rule-name-starts-with": "Правила, імена яких починаються з '{{prefix}}'", - "type-plugin": "Плагін", - "type-plugins": "Плагіни", - "list-of-plugins": "{ count, plural, 1 {Один плагін} other {Список # плагінів} }", - "plugin-name-starts-with": "Плагіни, імена яких починаються з '{{prefix}}'", - "type-tenant": "Власник", - "type-tenants": "Власники", - "list-of-tenants": "{ count, plural, 1 {Один власник} other {Список # власників} }", - "tenant-name-starts-with": "Власники, імена яких починаються з '{{prefix}}'", - "type-customer": "Клієнт", - "type-customers": "Клієнти", - "list-of-customers": "{ count, plural, 1 {Один клієнт} other {Список # клієнтів} }", - "customer-name-starts-with": "Клієнти, імена яких починаються з '{{prefix}}'", - "type-user": "Користувач", - "type-users": "Користувачі", - "list-of-users": "{ count, plural, 1 {Один користувач} other {Список # користувачів } }", - "user-name-starts-with": "Користувачі, імена яких починаються з '{{prefix}}'", - "type-dashboard": "Панель візуалізації", - "type-dashboards": "Панелі візуалізації", - "list-of-dashboards": "{ count, plural, 1 {Одна панель візуалізації} other {Список # панелей візуалізації} }", - "dashboard-name-starts-with": "Панелі візуалізації, імена яких починаються з '{{prefix}}'", - "type-alarm": "Сигнал тривоги", - "type-alarms": "Сигнали тривоги", - "list-of-alarms": "{ count, plural, 1 {Один сигнал тривоги} other {Список # сигналів тривоги} }", - "alarm-name-starts-with": "Сигнали тривоги, імена яких починаються '{{prefix}}'", - "type-rulechain": "Правило ланцюжка", - "type-rulechains": "Правило ланцюжків", - "list-of-rulechains": "{ count, plural, 1 {Одне правило ланцюжка} other {Список # правил ланцюжків} }", - "rulechain-name-starts-with": "Правило ланцюжків, імена яких починаються '{{prefix}}'", - "type-scheduler-event": "Scheduler event", - "type-scheduler-events": "Scheduler events", - "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", - "scheduler-event-name-starts-with": "Scheduler events whose names start with '{{prefix}}'", - "type-blob-entity": "Blob entity", - "type-blob-entities": "Blob entities", - "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", - "blob-entity-name-starts-with": "Blob entities whose names start with '{{prefix}}'", - "type-rulenode": "Правило", - "type-rulenodes": "Правила", - "list-of-rulenodes": "{ count, plural, 1 {Одне правило} other {Список # правил} }", - "rulenode-name-starts-with": "Список правил, імена яких починаються '{{prefix}}'", - "type-current-customer": "Поточний клієнт", - "search": "Пошук сутностей", - "selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано", - "entity-name": "Ім'я сутності", - "details": "Подробиці сутності", - "no-entities-prompt": "Сутності не знайдено", - "no-data": "Немає даних для відображення", - "columns-to-display": "Стовпці для відображення", - "type-entity-group": "Група сутностей", - "type-converter": "Перетворювач даних", - "type-converters": "Перетворювачі даних", - "list-of-converters": "{ count, plural, 1 {Однин перетворювач даних} other {Список # перетворювачів даних} }", - "converter-name-starts-with": "Перетворювачі даних, імена яких починаються з '{{prefix}}'", - "type-integration": "Інтеграція", - "type-integrations": "Інтеграції", - "list-of-integrations": "{ count, plural, 1 {Одна інтеграція} other {Список # інтеграцій} }", - "integration-name-starts-with": "Інтеграції, імена яких починаються з '{{prefix}}'" - }, - "entity-group": { - "entity-group": "Група сутності", - "details": "Деталі", - "columns": "Стовпці", - "add-column": "Додати стовпець", - "column-value": "Значення", - "column-value-required": "Необхідно вказати значення.", - "column-title": "Назва", - "default-sort-order": "Основний порядок сортування", - "default-sort-order-required": "Необхідно вказати основний порядок сортування.", - "hide-in-mobile-view": "Мобільний приховано", - "use-cell-style-function": "Використовувати функцію стилю комірки", - "use-cell-content-function": "Use cell content function", - "edit-column": "Редагувати стовпець", - "column-details": "Деталі стовпця", - "actions": "Дії", - "settings": "Налаштування", - "delete": "Видалити групу сутностей", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "add": "Додати групу сутностей", - "add-entity-group-text": "Додати нову групу сутностей", - "no-entity-groups-text": "Не знайдено жодних груп сутності", - "entity-group-details": "Деталі групи сутності", - "delete-entity-groups": "Видалити групи сутностей", - "delete-entity-group-title": "Ви впевнені, що хочете видалити групу сутності '{{entityGroupName}}'?", - "delete-entity-group-text": "Будьте обережні, після підтвердження, група сутностей і всі пов'язані з нею дані стануть недоступними.", - "delete-entity-groups-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }?", - "delete-entity-groups-action-title": "Видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }", - "delete-entity-groups-text": "Будьте обережні, після підтвердження, всі виділені групи сутностей і пов'язані з ними дані, стануть недоступними.", - "device-groups": "Групи пристроїв", - "asset-groups": "Групи активів", - "customer-groups": "Групи клієнтів", - "device-group": "Група пристроїв", - "asset-group": "Група активів", - "customer-group": "Група клієнтів", - "fetch-more": "Отримати більше", - "column-type": { - "column-type": "Тип стовпця", - "client-attribute": "Атрибут клієнта", - "shared-attribute": "Спільний атрибут", - "server-attribute": "Атрибут сервера", - "timeseries": "Телеметрія", - "entity-field": "Поле сутності" - }, - "column-type-required": "Необхідно вказати тип стовпця.", - "entity-field": { - "created-time": "Створений час", - "name": "Ім'я", - "type": "Тип", - "assigned_customer": "Призначений клієнт", - "authority": "Авторитет", - "first_name": "Ім'я", - "last_name": "Прізвище", - "email": "Електронна пошта", - "title": "Назва", - "country": "Країна", - "state": "Штат", - "city": "Місто", - "address": "Адреса", - "address2": "Адреса 2", - "zip": "Zip", - "phone": "Телефон" - }, - "sort-order": { - "asc": "У порядку зростання", - "desc": "У порядку зменшення", - "none": "Не має" - }, - "details-mode": { - "on-row-click": "Клацніть на рядок", - "on-action-button-click": "Клацніть на кнопку детелі", - "disabled": "Вимкнено" - }, - "add-to-group": "Додати до групи", - "move-to-group": "Перемістити до групи", - "select-entity-group": "Виберати групу сутностей", - "no-entity-groups-matching": "Не знайдено жодних груп сутностей, що відповідають '{{entityGroup}}'.", - "target-entity-group-required": "Необхідно вказати цільову групу сутності.", - "remove-from-group": "Видалити з групи", - "group-table-title": "Group table title", - "enable-search": "Увімкнути пошук сутностей", - "enable-add": "Увімкнути додавання сутностей", - "enable-delete": "Увімкнути видалення сутностей", - "enable-selection": "Увімкнути вибір сутностей", - "enable-group-transfer": "Увімкнути дії групового перенесення", - "display-pagination": "Відображення сторінок", - "default-page-size": "Розмір сторінки за замовчуванням", - "enable-assignment-actions": "Увімкнути дії призначення", - "enable-credentials-management": "Увімкнути керування авторизаційними даними", - "enable-users-management": "Увімкнути керування користувачами", - "enable-assets-management": "Увімкнути керування активами", - "enable-devices-management": "Увімкнути керування пристроями", - "enable-dashboards-management": "Увімкнути керування панелями візуалізації", - "open-details-on": "Відкрити деталі сутності", - "select-existing": "Виберіть існуючу групу сутностей", - "create-new": "Створити нову групу сутностей", - "new-entity-group-name": "Нове ім'я групи сутностей", - "entity-group-list": "Список групи сутностей", - "entity-group-list-empty": "Не вибрано жодної групи сутностей.", - "name-starts-with": "Назва групи сутностей починається з", - "entity-group-name-filter-required": "Необхідно задати назву групи сутностей." - }, - "entity-view": { - "entity-view": "Перегляд сутності", - "entity-view-required": "Необхідно вказати перегляд сутності.", - "entity-views": "Перегляди сутностей", - "management": "Керування переглядом сутностей", - "view-entity-views": "Переглянути перегляд сутностей", - "entity-view-alias": "Псевдонім перегляду сутності", - "aliases": "Псевдоніми перегляду сутності", - "no-alias-matching": "Псевдонім'{{alias}}' не знайдено.", - "no-aliases-found": "Псевдоніми не знайдено.", - "no-key-matching": "'Ключ {{key}}' не знайдено.", - "no-keys-found": "Ключі не знайдено.", - "create-new-alias": "Створити новий!", - "create-new-key": "Створити новий!", - "duplicate-alias-error": "Псевдонім з такою назвою вже існує '{{alias}}'.
Псевдоніми перегляду повинні бути унікальними на панелі візуалізації.", - "configure-alias": "Налаштувати псевдонім '{{alias}}'", - "no-entity-views-matching": "Сутності, які відповідають '{{entity}}' не знайдені.", - "alias": "Псевдонім", - "alias-required": "Необхідно вказати псевдонім перегляду сутності.", - "remove-alias": "Видалити псевдонім перегляду сутності", - "add-alias": "Додати псевдонім перегляду сутності", - "name-starts-with": "Ім'я перегляду сутності починається з", - "entity-view-list": "Список перегляду сутності", - "use-entity-view-name-filter": "Використати фільтр", - "entity-view-list-empty": "Не вибрано жодного перегляду сутності.", - "entity-view-name-filter-required": "Необхідно вказвти фільтр назв представлення сутності.", - "entity-view-name-filter-no-entity-view-matched": "Перегляди сутностей, назви яких починаються з '{{entityView}}' не знайдено.", - "add": "Додати перегляд сутності", - "assign-to-customer": "Призначити клієнту", - "assign-entity-view-to-customer": "Призначити перегляд(и) сутності(ей) клієнту", - "assign-entity-view-to-customer-text": "Будь ласка, виберіть перегляд сутності для призначення клієнту", - "no-entity-views-text": "Перегляду сутності не знайдено", - "assign-to-customer-text": "Будь ласка, виберіть клієнта, для призначиення перегляду(ів) сутності(ей)", - "entity-view-details": "Деталі перегляду сутності", - "add-entity-view-text": "Додати новий перегляд сутносі", - "delete": "Видалити перегляд сутності", - "assign-entity-views": "Призначити перегляд сутності", - "assign-entity-views-text": "Призначити { count, plural, 1 {1 перегляд сутності} other {# перегляди сутностей } } клієнту", - "delete-entity-views": "Видалити перегляди сутностей", - "unassign-from-customer": "Позбавити клієнта", - "unassign-entity-views": "Позбавити переглядів сутностей", - "unassign-entity-views-action-title": "Позбавити { count, plural, 1 {1 перегляду сутності} other {# переглядів сутностей} } клієнта", - "assign-new-entity-view": "Призначити новий перегляд сутності", - "delete-entity-view-title": "Ви впевнені, що хочете видалити перегляд сутності'{{entityViewName}}'?", - "delete-entity-view-text": "Будьте обережні, після підтвердження, перегляд сутності та всі пов'язані з нею дані стануть недоступними.", - "delete-entity-views-title": "Ви впевнені, що хочете видалити перегляд сутності { count, plural, 1 {1 перегляд сутності } other {# перегляди сутностей } }?", - "delete-entity-views-action-title": "Видалити { count, plural, 1 {1 перегляд сутності } other {# перегляди сутностей } }", - "delete-entity-views-text": "Будьте обережні, після підтвердження, всі виділені перегляди сутностей та дні, пов'язані з ними стануть недоступними.", - "unassign-entity-view-title": "Ви впевнені, що хочете позбавити перегляду сутності '{{entityViewName}}'?", - "unassign-entity-view-text": "Після підтвердження клієнт буде позбавлений перегляду сутності. Дані перегляду сутності не будуть доступні клієнту.", - "unassign-entity-view": "Позбавити перегляду сутності", - "unassign-entity-views-title": "Ви впевнені, що хочете позбавити { count, plural, 1 {1 перегляду сутності} other {# переглядів сутностей} }?", - "unassign-entity-views-text": "Після підтвердження, клієнта буде позбавлено всіх виділених переглядів сутності. Дані переглядів сутностей не будуть доступні клієнту .", - "entity-view-type": "Тип перегляду сутності", - "entity-view-type-required": "Необхідно вказати тип перегляду сутності.", - "select-entity-view-type": "Виберіть тип перегляду сутності", - "enter-entity-view-type": "Введіть тип перегляду сутності", - "any-entity-view": "Будь-який перегляд сутності", - "no-entity-view-types-matching": "Не знайдено жодних типів перегляду сутності, що відповідають '{{entitySubtype}}'.", - "entity-view-type-list-empty": "Не вибрано тип перегляду сутності.", - "entity-view-types": "Типи перегляду сутності", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "events": "Події", - "details": "Деталі", - "copyId": "Скопіювати Id перегляду сутності", - "assignedToCustomer": "Призначений клієнту", - "unable-entity-view-device-alias-title": "Неможливо видалити псевдонім перегляду сутності", - "unable-entity-view-device-alias-text": "Не вдалося видалити псевдонім пристрою'{{entityViewAlias}}', так як він використовується наступним(и) віджетом(ами):
{{widgetsList}}", - "select-entity-view": "Виберати перегляд сутності", - "make-public": "Зробити перегляд сутності публічним", - "start-date": "Дата початку", - "start-ts": "Час початку", - "end-date": "Дата закінчення", - "end-ts": "Час завершення", - "date-limits": "Обмеження дати", - "client-attributes": "Атрибути клієнта", - "shared-attributes": "Спільні атрибути", - "server-attributes": "Атрибути сервера", - "timeseries": "Телеметрія", - "client-attributes-placeholder": "Атрибути клієнта", - "shared-attributes-placeholder": "Спільні атрибути", - "server-attributes-placeholder": "Атрибути сервера", - "timeseries-placeholder": "Телеметрія", - "target-entity": "Цільова сутність", - "attributes-propagation": "Поширення атрибутів", - "attributes-propagation-hint": "Перегляд сутностей автоматично копіюватиме вказані атрибути з цільової сутності кожного разу, коли ви зберігаєте або оновлюєте ці перегляди. В цілях продуктивності, атрибути цільової сутності не поширюються на представлення сутності на кожній зміні їх атрибутів. Можна ввімкнути автоматичне поширення, налаштувавши у вашому ланцюжку правило \"copy to view\" і пов'язуючи його з повідомленнями типу \"Post attributes\" і \"Attributes Updated\"..", - "timeseries-data": "Дані телеметрії", - "timeseries-data-hint": "Налаштуйте ключі даних телеметрії цільової сутності, які будуть доступні перегляду сутності. Ці дані доступні лише для читання." - }, - "event": { - "events": "Події", - "event-type": "Тип події", - "type-error": "Помилка", - "type-lc-event": "Подія життєвого циклу", - "type-stats": "Статистика", - "type-debug-converter": "Налагоджувати", - "type-debug-integration": "Налагоджувати", - "type-debug-rule-node": "Налагоджувати", - "type-debug-rule-chain": "Налагоджувати", - "no-events-prompt": "Не знайдено жодних подій", - "error": "Помилка", - "alarm": "Сигнал тривоги", - "event-time": "Час події", - "server": "Сервер", - "body": "Тіло", - "method": "Метод", - "type": "Тип", - "in": "In", - "out": "Out", - "metadata": "Метадані", - "message": "Повідомлення", - "entity": "Сутність", - "message-id": "Id повідомлення", - "message-type": "Тип повідомлення", - "data-type": "Тип даних", - "relation-type": "Тип зв'язку", - "data": "Дані", - "event": "Подія", - "status": "Статус", - "success": "Успіх", - "failed": "Невдача", - "messages-processed": "Повідомлення опрацьовані", - "errors-occurred": "Виникли помилки" - }, - "extension": { - "extensions": "Розширення", - "selected-extensions": " вибрано { count, plural, 1 {1 розширення} other {# розширення} }", - "type": "Тип", - "key": "Ключ", - "value": "Значення", - "id": "Id", - "extension-id": "Id розширення", - "extension-type": "Тип розширення", - "transformer-json": "JSON *", - "unique-id-required": "Таке Id розширення вже існує.", - "delete": "Видалити розширення", - "add": "Додати розширення", - "edit": "Редагувати розширення", - "delete-extension-title": "Ви дійсно бажаєте видалити розширення '{{extensionId}}'?", - "delete-extension-text": "Будьте обережні, після підтвердження, розширення та всі пов'язані з ним дані стануть недоступними.", - "delete-extensions-title": "Ви дійсно бажаєте видалити { count, plural, 1 {1 розширення} other {# розширення} }?", - "delete-extensions-text": "Будьте обережні, після підтвердження, всі вибрані розширення будуть видалені.", - "converters": "Перетворювачі", - "converter-id": "Id перетворювача", - "configuration": "Конфігурація", - "converter-configurations": "Конфігурації перетворювача", - "token": "Маркер безпеки", - "add-converter": "Додати конвертер", - "add-config": "Додати конфігурацію конвертера", - "device-name-expression": "Маска імені пристрою", - "device-type-expression": "Маска типу пристрою", - "custom": "Користувач", - "to-double": "Подвоїти", - "transformer": "Трансформатор", - "json-required": "Необхідно вказати json трансформатора.", - "json-parse": "Неможливо проаналізувати json трансформатора.", - "attributes": "Атрибути", - "add-attribute": "Додати атрибут", - "add-map": "Додати елемент відображення", - "timeseries": "Телеметрія", - "add-timeseries": "Додати параметри телеметрії", - "field-required": "Field is required", - "brokers": "Брокери", - "add-broker": "Додати брокера", - "host": "Хост", - "port": "Порт", - "port-range": "Значення порту має бути в діапазоні від 1 до 65535.", - "ssl": "Ssl", - "credentials": "Авторизаційні дані", - "username": "Ім'я користувача", - "password": "Пароль", - "retry-interval": "Інтервал повтору в мілісекундах", - "anonymous": "Анонімний", - "basic": "Основний", - "pem": "PEM", - "ca-cert": "Файл CA сертифіката *", - "private-key": "Файл приватного ключа *", - "cert": "Файл сертифіката *", - "no-file": "Не вибрано жодного файлу.", - "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", - "mapping": "Зіставлення", - "topic-filter": "Фільтр тем", - "converter-type": "Тип конвертера", - "converter-json": "Json", - "json-name-expression": " Json вираз для назви пристрою", - "topic-name-expression": "Вираз для назви пристрою в назві теми", - "json-type-expression": "Json вираз для типу пристрою", - "topic-type-expression": "Вираз для типу пристрою в назві теми", - "attribute-key-expression": "Вираз для ключа атрибута", - "attr-json-key-expression": " Json вираз для ключа атрибута", - "attr-topic-key-expression": "Вираз для ключа атрибута в назві теми", - "request-id-expression": "Вираз для id запиту", - "request-id-json-expression": "Json вираз для id запиту", - "request-id-topic-expression": "Вираз для id запиту в назві теми", - "response-topic-expression": "Вираз для теми відповідей", - "value-expression": "Вираз для значення", - "topic": "Тема", - "timeout": "Час очікування в мілісекундах", - "converter-json-required": "Необхідно вказати json конвертер.", - "converter-json-parse": "Неможливо проаналізувати json конвертера.", - "filter-expression": "Вираз для фільтра", - "connect-requests": "Запити на підключення", - "add-connect-request": "Додати запит на підключення", - "disconnect-requests": "Відключення запитів", - "add-disconnect-request": "Додати запит на відключення", - "attribute-requests": "Запити атрибутів", - "add-attribute-request": "Додати запит атрибута", - "attribute-updates": "Оновлення атрибутів", - "add-attribute-update": "Додати оновлення атрибутів", - "server-side-rpc": "Серверна сторона RPC", - "add-server-side-rpc-request": "Додати RPC-запит на стороні сервера", - "device-name-filter": "Фільтр назви пристрою", - "attribute-filter": "Фільтр атрибутів", - "method-filter": "Фільтр методів", - "request-topic-expression": "Вираз для теми запитів", - "response-timeout": "Час очікування відповіді в мілісекундах", - "topic-expression": "Вираз для назви теми", - "client-scope": "Обсяг клієнта", - "add-device": "Додати пристрій", - "opc-server": "Сервери", - "opc-add-server": "Додати сервер", - "opc-add-server-prompt": "Будь ласка, додайте сервер", - "opc-application-name": "Назва програми", - "opc-application-uri": "URI програми", - "opc-scan-period-in-seconds": "Період сканування в секундах", - "opc-security": "Безпека", - "opc-identity": "Ідентифікація", - "opc-keystore": "Сховище ключів", - "opc-type": "Тип", - "opc-keystore-type": "Тип", - "opc-keystore-location": "Розташування *", - "opc-keystore-password": "Пароль", - "opc-keystore-alias": "Псевдонім", - "opc-keystore-key-password": "Пароль для ключа", - "opc-device-node-pattern": "Патерн OPC вузла пристрою", - "opc-device-name-pattern": "Патерн назви пристрою", - "modbus-server": "Сервери/ведені пристрої", - "modbus-add-server": "Додати сервер/ведений пристрій", - "modbus-add-server-prompt": "Будь ласка, додайте сервер/ведений пристрій", - "modbus-transport": "Транспорт", - "modbus-port-name": "Ім'я послідовного порту", - "modbus-encoding": "Кодування", - "modbus-parity": "Паритет", - "modbus-baudrate": "Швидкість передачі даних", - "modbus-databits": "Біти даних", - "modbus-stopbits": "Стоп-біти", - "modbus-databits-range": "Біти даних повинні знаходитися в діапазоні від 7 до 8.", - "modbus-stopbits-range": "Стоп-біти повинні знаходитися в діапазоні від 1 до 2.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", - "modbus-device-name": "Ім'я пристрою", - "modbus-poll-period": "Період опитування (мс)", - "modbus-attributes-poll-period": "Період опитування атрибутів (мс)", - "modbus-timeseries-poll-period": "Період опитування телеметрії (мс)", - "modbus-poll-period-range": "Період опитування повинен бути більше 0.", - "modbus-tag": "Тег", - "modbus-function": "Modbus функція", - "modbus-register-address": "Адреса регістру ", - "modbus-register-address-range": "Адреса регістру повинна бути в діапазоні від 0 до 65535.", - "modbus-register-bit-index": "Номер бітів", - "modbus-register-bit-index-range": "Номер бітів повинен знаходитися в діапазоні від 0 до 15.", - "modbus-register-count": "Рахунок регістру", - "modbus-register-count-range": "Рахунок регістру повинен бути більше 0.", - "modbus-byte-order": "Порядок байтів", - "sync": { - "status": "Статус", - "sync": "Синхронізований", - "not-sync": "Не синхронізований", - "last-sync-time": "Час останньої синхронізації", - "not-available": "Недоступний" - }, - "export-extensions-configuration": "Експортувати конфігурацію розширень", - "import-extensions-configuration": "Імпортувати конфігурацію розширень", - "import-extensions": "Імпортувати розширення", - "import-extension": "Імпортувати розширення", - "export-extension": "Експортувати розширення", - "file": "Файл розширень", - "invalid-file-error": "Не правильний формат файла" - }, - "fullscreen": { - "expand": "Відкрити у повноекранному режимі", - "exit": "Вийти з повноекранного режиму", - "toggle": "Перемкнути повноекранний режим", - "fullscreen": "Повноекранний режим" - }, - "function": { - "function": "Функція" - }, - "grid": { - "delete-item-title": "Ви впенені, що хочете видалити цей елемент?", - "delete-item-text": "Будьте обережні, після підтвердження, цей елемент і всі пов'язані з ним дані, стануть недоступними.", - "delete-items-title": "Ви впенені, що хочете видалити { count, plural, 1 {1 елемент} other {# елементи} }?", - "delete-items-action-title": "Видалити{ count, plural, 1 {1 елемент} other {# елементи} }", - "delete-items-text": "Будьте обережні, після підтвердження, всі виділені елементи і пов'язані з ними дані, стануть недоступними.", - "add-item-text": "Додати новий елемент", - "no-items-text": "Не знайдено жодного елемента", - "item-details": "Деталі елемента", - "delete-item": "Видалити елементи", - "scroll-to-top": "Перейти угору" - }, - "help": { - "goto-help-page": "Перейти на сторінку довідки" - }, - "home": { - "home": "Домашня сторінка", - "profile": "Профіль", - "logout": "Вийти", - "menu": "Меню", - "avatar": "Аватар", - "open-user-menu": "Відкрити меню користувача" - }, - "import": { - "no-file": "Не вибрано жодного файлу", - "drop-file": "Перетягніть JSON файл, або клацніть, щоб вибрати файл для завантаження.", - "drop-csv-file": "Перетягніть CSV файл, або клацніть, щоб вибрати файл для завантаження." - }, - "integration": { - "integration": "Інтеграція", - "integrations": "Інтеграції", - "select-integration": "Виберіть інтеграцію", - "no-integrations-matching": "Не знайдено жодних інтеграцій, які відповідають '{{entity}}'.", - "integration-required": "Необхідно вказати інтеграцію", - "delete": "Видалити інтеграцію", - "management": "Управління інтеграціями", - "add-integration-text": "Додати нову інтеграцію", - "no-integrations-text": "Не знайдено жодної інтеграції", - "selected-integrations": "{ count, plural, 1 {1 інтеграція} other {# інтеграції} } вибрано", - "delete-integration-title": "Ви впевнені, що хочете видалити інтеграцію '{{integrationName}}'?", - "delete-integration-text": "Будьте обережні, після підтвердження інтеграція та всі пов'язані з нею дані стануть недоступними.", - "delete-integrations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }?", - "delete-integrations-action-title": "Видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }", - "delete-integrations-text": "Будьте обережні, після підтвердження всі вибрані інтеграції будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "events": "Події", - "add": "Додати інтеграцію", - "integration-details": "Деталі інтеграції", - "details": "Деталі", - "copyId": "Копіювати Id інтеграції", - "idCopiedMessage": "Id інтеграції скопійовано в буфер обміну", - "debug-mode": "Режим налагодження", - "enable-security": "Увімкнути безпеку", - "headers-filter": "Заголовки фільтра", - "header": "Заголовок", - "no-headers-filter": "Немає фільтрів заголовків", - "downlink-url": "Downlink URL", - "application-uri": "URI програми", - "as-id": "AS ID", - "as-id-required": "Необхідно вказати AS ID.", - "as-key": "AS ключ", - "as-key-required": "Необхідно вказати AS ключ.", - "max-time-diff-in-seconds": "Максимальна різниця в часі (секунди)", - "max-time-diff-in-seconds-required": "Необхідно вказати максимальну різницю в часі.", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "base-url": "Базова URL-адреса", - "base-url-required": "Необхідно вказати базову URL-адресу", - "security-key": "Ключ захисту", - "http-endpoint": "URL кінцевої точки HTTP", - "copy-http-endpoint-url": "Скопіювати URL-адресу кінцевої точки HTTP", - "http-endpoint-url-copied-message": "URL кінцевої точки HTTP скопійовано в буфер обміну", - "host": "Хост", - "host-required": "Необхідно вказати хост.", - "host-type": "Тип хоста", - "host-type-required": "Необхідно вказати тип хоста.", - "custom-host": "Хост користувача", - "custom-host-required": "Необхідний спеціальний хост.", - "port": "Порт", - "port-required": "Необхідно вказати порт.", - "port-range": "Порт має бути в діапазоні від 1 до 65535.", - "connect-timeout": "Час очікування з'єднання (сек)", - "connect-timeout-required": " Необхідно вказати час з'єднання підключення.", - "connect-timeout-range": "Час очікування з'єднання має бути в діапазоні від 1 до 200.", - "client-id": "ID клієнта", - "clean-session": "Очистити сеанс", - "enable-ssl": "Увімкнути SSL", - "credentials": "Авторизаційні дані", - "credentials-type": "Тип авторизаційних даних", - "credentials-type-required": "Необхідно вказати тип авторизаційних даних.", - "username": "Ім'я користувача", - "username-required": "Необхідно вказати ім'я користувача.", - "password": "Пароль", - "password-required": "Необхідно вказати пароль.", - "ca-cert": "Файл сертифіката CA *", - "private-key": "Файл приватного ключа *", - "private-key-password": "Пароль приватного ключа", - "cert": "Файл сертифіката*", - "no-file": "Не вибрано жодного файлу.", - "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", - "topic-filters": "Тематичні фільтри", - "remove-topic-filter": "Видалити фільтр тем", - "add-topic-filter": "Додати фільтр тем", - "add-topic-filter-prompt": "Будь ласка, додайте фільтр тем", - "topic": "Тема", - "mqtt-qos": "QoS", - "mqtt-qos-at-most-once": "Не більше одного разу", - "mqtt-qos-at-least-once": "Принаймні, один раз", - "mqtt-qos-exactly-once": "Точно один раз", - "downlink-topic-pattern": "Downlink topic pattern", - "downlink-topic-pattern-required": "Downlink topic pattern is required.", - "aws-iot-endpoint": "AWS IoT Endpoint", - "aws-iot-endpoint-required": "AWS IoT Endpoint is required.", - "aws-iot-credentials": "Авторизаційні дані AWS IoT", - "application-credentials": "Авторизаційні дані додатків", - "api-key": "API ключ", - "api-key-required": "Необхідно вказати API ключ.", - "auth-token": "Маркер аутентифікації", - "auth-token-required": "Необхідно вказати маркер аутентифікації.", - "region": "Регіон", - "region-required": "Необхідно вказати регіон.", - "application-id": "ID програми", - "application-id-required": "Необхідно вказати ID програми.", - "access-key": "Ключ доступу", - "access-key-required": "Необхідно вказати ключ доступу.", - "connection-parameters": "Параметри підключення", - "service-bus-namespace-name": "Service Bus Namespace Name", - "service-bus-namespace-name-required": "Необхідно вказати Service Bus Namespace Name is required.", - "event-hub-name": "Event Hub Name", - "event-hub-name-required": "Необхідно вказати Event Hub Name is required.", - "sas-key-name": "Назва ключа SAS", - "sas-key-name-required": "Необхідно вказати назву ключа SAS.", - "sas-key": "Ключ SAS", - "sas-key-required": "SAS Key is required.", - "iot-hub-name": "IoT Hub Name (required for downlink)", - "metadata": "Метадані", - "type": "Тип", - "type-required": "Необхідно вказати тип.", - "uplink-converter": "Конвертер передачі даних", - "uplink-converter-required": "Необхідно вказати конвертер передачі даних.", - "downlink-converter": "Downlink data converter", - "type-http": "HTTP", - "type-ocean-connect": "OceanConnect", - "type-sigfox": "SigFox", - "type-thingpark": "ThingPark", - "type-tmobile-iot-cdp": "T-Mobile – IoT CDP", - "type-mqtt": "MQTT", - "type-aws-iot": "AWS IoT", - "type-ibm-watson-iot": "IBM Watson IoT", - "type-ttn": "TheThingsNetwork", - "type-azure-event-hub": "Azure Event Hub", - "type-opc-ua": "OPC-UA", - "type-ffb": "FFB", - "opc-ua-application-name": "Назва програми", - "opc-ua-application-uri": "Application uri", - "opc-ua-scan-period-in-seconds": "Період сканування в секундах", - "opc-ua-scan-period-in-seconds-required": "Необхідно вказати період сканування в секундах", - "opc-ua-timeout": "Час очікування в мілісекундах", - "opc-ua-timeout-required": "Необхідно вказати час очікування в мілісекундах", - "opc-ua-security": "Безпека", - "opc-ua-security-required": "Необхідно задати безпеку", - "opc-ua-identity": "Ідентифікація", - "opc-ua-identity-required": "Необхідно вказати ідентифікацію", - "opc-ua-keystore": "Сховище ключів", - "add-opc-ua-keystore-prompt": "Будь ласка, додайте файл сховища ключів", - "opc-ua-keystore-required": "Необхідно вказати сховище ключів", - "opc-ua-type": "Тип", - "opc-ua-keystore-type": "Тип сховища ключів", - "opc-ua-keystore-type-required": "Необхідно вказати тип", - "opc-ua-keystore-location": "Розташування *", - "opc-ua-keystore-password": "Пароль", - "opc-ua-keystore-password-required": "Необхідно вказати пароль", - "opc-ua-keystore-alias": "Псевдонім", - "opc-ua-keystore-alias-required": "Необхідно вказати псевдонім", - "opc-ua-keystore-key-password": "Пароль ключа", - "opc-ua-keystore-key-password-required": "Необхідно вказати пароль ключа", - "opc-ua-mapping": "Зіставлення", - "add-opc-ua-mapping-prompt": "Будь ласка, додайте зіставлення", - "opc-ua-mapping-type": "Тип зіставлення", - "opc-ua-mapping-type-required": "Необхідно вказати тип зіставлення", - "opc-ua-device-node-pattern": "Шаблон вузла пристрою", - "opc-ua-device-node-pattern-required": "Необхідно вказати шаблон вузла пристрою", - "opc-ua-add-map": "Додати елемент зіставлення", - "subscription-tags": "Теги передплати Теги підписки", - "remove-subscription-tag": "Видалити тег підписки", - "add-subscription-tag": "Додати тег підписки", - "add-subscription-tag-prompt": "Будь ласка, додайте тег підписки", - "key": "Ключ", - "path": "Шлях", - "required": "Необхідно" - }, - "item": { - "selected": "Вибрані" - }, - "js-func": { - "no-return-error": "Функція повинна повертати значення!", - "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!", - "tidy": "Tidy" - }, - "key-val": { - "key": "Ключ", - "value": "Значення", - "remove-entry": "Видалити елемент", - "add-entry": "Додати елемент", - "no-data": "Елементи відсутні" - }, - "layout": { - "layout": "Макет", - "manage": "Керування макетами", - "settings": "Налаштування макета", - "color": "Колір", - "main": "Основний", - "right": "Правий", - "select": "Вибрати макет" - }, - "legend": { - "direction": "Розташування елементів легенди", - "position": "Розташування легенди", - "show-max": "Показати максимальне значення", - "show-min": "Показати мінімальне значення ", - "show-avg": "Показати середнє значення", - "show-total": "Показати суму", - "settings": "Налаштування легенди", - "min": "мін", - "max": "макс", - "avg": "середнє", - "total": "Сума" - }, - "login": { - "login": "Вхід", - "request-password-reset": "Запит скидання пароля", - "reset-password": "Скинути пароль", - "create-password": "Створити пароль", - "passwords-mismatch-error": "Введені паролі повинні бути однаковими!", - "password-again": "Введіть пароль ще раз", - "sign-in": "Будь ласка, увійдіть в систему", - "username": "Ім'я користувача (ел. пошта)", - "remember-me": "Запам'ятати мене", - "forgot-password": "Забули пароль?", - "password-reset": "Скидання пароля", - "new-password": "Новий пароль", - "new-password-again": "Повторіть новий пароль", - "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!", - "email": "Електронна пошта" - }, - "position": { - "top": "Угорі", - "bottom": "Знизу", - "left": "Ліворуч", - "right": "Праворуч" - }, - "profile": { - "profile": "Профіль", - "change-password": "Змінити пароль", - "current-password": "Поточний пароль" - }, - "relation": { - "relations": "Відношення", - "direction": "Напрямок", - "search-direction": { - "FROM": "З", - "TO": "До" - }, - "direction-type": { - "FROM": "з", - "TO": "до" - }, - "from-relations": "Вихідні відношення", - "to-relations": "Вхідні відношення", - "selected-relations": "Вибрано { count, plural, 1 {1 відношення} other {# відношення} }", - "type": "Тип", - "to-entity-type": "До типу сутності", - "to-entity-name": "До імені сутності", - "from-entity-type": "Від типу сутності", - "from-entity-name": "Від імені сутності", - "to-entity": "До сутності", - "from-entity": "Від сутності", - "delete": "Видалити відношення", - "relation-type": "Тип відношення", - "relation-type-required": "Необхідно вказати тип відношення.", - "any-relation-type": "Будь-який тип", - "add": "Додати відношення", - "edit": "Редагувати відношення", - "delete-to-relation-title": "Ви впевнені, що хочете видалити відношення до сутності '{{entityName}}'?", - "delete-to-relation-text": "Будьте обережні, після підтвердження, сутність '{{entityName}}' не буде пов'язана з поточним об'єктом.", - "delete-to-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", - "delete-to-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення стануть не пов'язані з поточною сутністю.", - "delete-from-relation-title": "Ви впевнені, що хочете видалити зв'язок, який йде від сутності '{{entityName}}'?", - "delete-from-relation-text": "Будьте обережні, після підтвердження поточна сутність буде відв'язана від сутності '{{entityName}}'.", - "delete-from-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", - "delete-from-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення будуть видалені, а поточна сутність стане не зв'язаною з відповідними сутностями.", - "remove-relation-filter": "Видалити фільтр відношення", - "add-relation-filter": "Додати фільтр відношення", - "any-relation": "Будь-яке відношення", - "relation-filters": "Фільтри відношення", - "additional-info": "Додаткова інформація (JSON)", - "invalid-additional-info": "Не вдалося розібрати JSON з додатковою інформацією ." - }, - "rulechain": { - "rulechain": "Ланцюг правил", - "rulechains": "Ланцюги правил", - "root": "Основний", - "delete": "Видалити ланцюг правил", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "description": "Опис", - "add": "Додати ланцюг правил", - "set-root": "Зробити ланцюг правил основним", - "set-root-rulechain-title": "Ви впевнені, що хочете зробити ланцюг правил '{{ruleChainName}}' основним?", - "set-root-rulechain-text": "Після підтвердження ланцюг правил стане основним і буде обробляти всі вхідні транспортні повідомлення.", - "delete-rulechain-title": "Ви впевнені, що хочете видалити ланцюг правил '{{ruleChainName}}'?", - "delete-rulechain-text": "Будьте обережні, після підтвердження ланцюг правил і всі пов'язані з ним дані стануть недоступними.", - "delete-rulechains-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }?", - "delete-rulechains-action-title": "Видалити{ count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }", - "delete-rulechains-text": "Будьте обережні, після підтвердження, вибрані ланцюги правил і всі пов'язані з ними дані стануть недоступними.", - "add-rulechain-text": "Додати новий ланцюг правил", - "no-rulechains-text": "Ланцюг правил не знайдено", - "rulechain-details": "Деталі ланцюга правил", - "details": "Деталі", - "events": "Події", - "system": "Система", - "import": "Імпортувати ланцюг правил", - "export": "Експортувати ланцюг правил", - "export-failed-error": "Не вдалося експортувати ланцюг правил: {{error}}", - "create-new-rulechain": "Створити новий ланцюг правил", - "rulechain-file": "Файл ланцюга правил", - "invalid-rulechain-file-error": "Неможливо імпортувати ланцюг правил: недійсна структуру даних ланцюга правил.", - "copyId": "Копіювати Id ланцюга правил", - "idCopiedMessage": "Id ланцюга правил скопійовано в буфер обміну", - "select-rulechain": "Вибрати ланцюг правил", - "no-rulechains-matching": "Не знайдено жодних ланцюгів правил, які відповідають '{{entity}}'.", - "rulechain-required": "Необхідно вказати ланцюг правил", - "management": "Управління ланцюгами правил", - "debug-mode": "Режим налагодження" - }, - "rulenode": { - "details": "Деталі", - "events": "Події", - "search": "Пошук вузлів", - "open-node-library": "Відкрити бібліотеку вузлів", - "add": "Додати вузол правил", - "name": "Ім'я", - "name-required": "Необхідно вказати ім'я.", - "type": "Тип", - "description": "Опис", - "delete": "Видалити вузол правил", - "select-all-objects": "Вибрати усі вузли та з'єднання", - "deselect-all-objects": "Зняти виділення з усіх вузлів і з'єднань", - "delete-selected-objects": "Видалити вибрані вузли та з'єднання", - "delete-selected": "Видалити вибране", - "select-all": "Вибрати все", - "copy-selected": "Копіювати вибране", - "deselect-all": "Відмінити вибране", - "rulenode-details": "Деталі вузла правил", - "debug-mode": "Режим налагодження", - "configuration": "Конфігурація", - "link": "Посилання", - "link-details": "Деталі посилання про вузол правил", - "add-link": "Додати посилання", - "link-label": "Мітка посилання", - "link-label-required": "Необхідно вказати мітку посилання.", - "custom-link-label": "Мітка посилання користувача", - "custom-link-label-required": "Необхідно вказати мітку посилання користувача.", - "link-labels": "Мітки посилання", - "link-labels-required": "Необхідно вказати мітки посилання.", - "no-link-labels-found": "Не знайдено жодних міток посилання", - "no-link-label-matching": "Мітка'{{label}}' не знайдена.", - "create-new-link-label": "Створити нову!", - "type-filter": "Filter", - "type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами", - "type-enrichment": "Насичення", - "type-enrichment-details": "Додати додаткову інформацію до метаданих повідомлень", - "type-transformation": "Трансформація", - "type-transformation-details": "Змінити склад повідомлення та його метадані", - "type-action": "Дія", - "type-action-details": "Виконати задану дію", - "type-analytics": "Аналітика", - "type-analytics-details": "Виконати аналіз потокових або збережених даних", - "type-external": "Зовнішній", - "type-external-details": "Взаємодіє з зовнішньою системою", - "type-rule-chain": "Ланцюг правил", - "type-rule-chain-details": "Перенаправити вхідне повідомлення на вказаний ланцюг правил", - "type-input": "Вхід", - "type-input-details": "Логічний вхід ланцюга правил, перенаправляє вхідні повідомлення на наступний пов'язаний вузол правил", - "type-unknown": "Невідомий", - "type-unknown-details": "Невизначений вузол правил", - "directive-is-not-loaded": "Вказана директива конфігурації '{{directiveName}}' недоступна.", - "ui-resources-load-error": "Не вдалося завантажити UI ресурси.", - "invalid-target-rulechain": "Не вдається визначити цільовий ланцюг правил!", - "test-script-function": "Протестувати скрипт", - "message": "Повідомлення", - "message-type": "Тип повідомлення", - "select-message-type": "Вибрати тип повідомлення", - "message-type-required": "Необхідно вказати тип повідомлення", - "metadata": "Метадані", - "metadata-required": "Записи метаданих не можуть бути порожніми.", - "output": "Вихід", - "test": "Тест", - "help": "Допомога" - }, - "scheduler": { - "scheduler": "Планувальник", - "scheduler-event": "Scheduler event Планування події. Запланувати подію. Подія планувальника", - "select-scheduler-event": "Виберати подію", - "no-scheduler-events-matching": "Не знайдено жодних подій, які відповідають '{{entity}}'.", - "scheduler-event-required": "Необхвдно вказати заплановану подію", - "management": "Управління планувальником", - "scheduler-events": "Планування подій", - "add-scheduler-event": "Додати подію", - "search-scheduler-events": "Пошук події", - "created-time": "Час створення", - "name": "Ім'я", - "type": "Тип", - "assigned_customer": "Призначений клієнт", - "edit-scheduler-event": "Редагувати подію", - "delete-scheduler-event": "Видалити подію", - "no-scheduler-events": "Не знайдено жодних запланованих подій", - "selected-scheduler-events": "{ count, plural, 1 {1 запланована подія} other {# заплановані події} } вибрано", - "delete-scheduler-event-title": "Ви впевнені, що хочете видалити подію '{{schedulerEventName}}'?", - "delete-scheduler-event-text": "Будьте обережні, після підтвердження подія і всі пов'язані з нею дані стануть недоступними.", - "delete-scheduler-events-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 запланована подія} other {# заплановані події} }?", - "delete-scheduler-events-text": "Будьте обережні, після підтвердження всі вибрані події будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "create": "Створити подію планувальника", - "edit": "Змінити подію планувальника", - "name-required": "Необхідно задати ім'я", - "configuration": "Конфігурація", - "schedule": "Розклад", - "start": "Початок", - "date": "Дата", - "time": "Час", - "repeat": "Повтор", - "repeats": "Повтори", - "daily": "Щодня", - "weekly": "Щотижня", - "repeats-required": "Потрібно вказати повторення.", - "repeat-on": "Повторити на", - "ends-on": "Завершити на", - "sunday-label": "Нд", - "monday-label": "Пн", - "tuesday-label": "Вт", - "wednesday-label": "Ср", - "thursday-label": "Чт", - "friday-label": "Пт", - "saturday-label": "Сб", - "repeat-on-sunday": "Повторити у неділю", - "repeat-on-monday": "Повторити в понеділок", - "repeat-on-tuesday": "Повторити у вівторок", - "repeat-on-wednesday": "Повторити в середу", - "repeat-on-thursday": "Повторити в червер", - "repeat-on-friday": "Повторити в п'ятницю", - "repeat-on-saturday": "Повторити в суботу", - "event-type": "Тип події", - "select-event-type": "Вибрати тип події", - "event-type-required": "Необхідно вказати типи події.", - "list-mode": "Перегляд списку", - "calendar-mode": "Перегляд календаря", - "calendar-view-type": "Тип перегляду календаря", - "month": "Місяць", - "week": "Тиждень", - "day": "День", - "agenda-week": "Порядок тижня", - "agenda-day": "Порялок дня", - "list-year": "Список року", - "list-month": "Список місяця", - "list-week": "Список тижня", - "list-day": "Список дня", - "today": "Сьогодні", - "navigate-before": "Перейти до", - "navigate-next": "Перейти далі", - "starting-from": "Починаючи з", - "until": "до", - "on": "на", - "sunday": "Неділя", - "monday": "Понеділок", - "tuesday": "Вівторок", - "wednesday": "Середа", - "thursday": "Четвер", - "friday": "П'ятниця", - "saturday": "Субота", - "originator": "Засновник", - "single-entity": "Самостійна сутність", - "group-of-entities": "Група сутностей", - "single-device": "Один пристрій", - "group-of-devices": "Група пристроїв", - "message-body": "Тіло повідомлення", - "target": "Ціль", - "rpc-method": "Метод", - "rpc-method-required": "Необхідно вказати метод", - "rpc-params": "Парами. Парні.", - "select-dashboard-state": "Виберіть стан панелі візуалізації" - }, - "report": { - "report-config": "Конфігурація звіту", - "email-config": "Конфігурація електронної пошти", - "dashboard-state-param": "Значення параметра стану панелі візуалізації", - "base-url": "Базова URL-адреса", - "base-url-required": "Необхідно вказати базову URL-адресу.", - "use-dashboard-timewindow": "Використовуйте вікно часу на панелі інструментів", - "timewindow": "Вікно часу", - "name-pattern": "Шаблон імені звіту", - "name-pattern-required": "Необхідно задати шаблон назви звіту", - "type": "Report type", - "use-current-user-credentials": "Використовувати поточні авторизаційні дані користувача", - "customer-user-credentials": "Авторизаційні дані користувачів", - "customer-user-credentials-required": "Необхідно задати авторизаційні дані користувачів", - "generate-test-report": "Створити звіт про перевірку", - "send-email": "Відправити лист", - "from": "Від", - "from-required": "Необхідно вказати від кого.", - "to": "До", - "to-required": "Необхідно вказати до кого.", - "cc": "Cc", - "bcc": "Bcc", - "subject": "Тема", - "subject-required": "Необхідно вказати тему.", - "body": "Тіло", - "body-required": "Необідно вказати тіло." - }, - "blob-entity": { - "blob-entity": "Blob сутності", - "select-blob-entity": "Вибрати blob сутності", - "no-blob-entities-matching": "Не знайдено жодних сутностей blob, які відповідають '{{entity}}'.", - "blob-entity-required": "Необхідно вказати blob сутності", - "files": "Файли", - "search": "Пошук файлів", - "clear-search": "Очистити пошук", - "no-blob-entities-prompt": "Файлів не знайдено", - "report": "Звіт", - "created-time": "Створено час", - "name": "Ім'я", - "type": "Тип", - "assigned_customer": "Призначений клієнт", - "download-blob-entity": "Завантажити файл", - "delete-blob-entity": "Видалити файл", - "delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?", - "delete-blob-entity-text": "Будьте обережні, після підствердження, дані файлу стануть недоступними." - }, - "timezone": { - "timezone": "Часовий пояс", - "select-timezone": "Виберати часовий пояс ", - "no-timezones-matching": "Не знайдено жодних часових поясів, які відповідають '{{timezone}}'.", - "timezone-required": "Необхідно вказати часовий пояс." - }, - "tenant": { - "tenant": "Власник", - "tenants": "Власники", - "management": "Управління власниками", - "add": "Додати власника", - "admins": "Адміністратори", - "manage-tenant-admins": "Керування адміністраторами власника", - "delete": "Видалити власника", - "add-tenant-text": "Додати нового власника", - "no-tenants-text": "Не знайдено жодного власника", - "tenant-details": "Подробиці про власника", - "delete-tenant-title": "Ви впевнені, що хочете видалити власника'{{tenantTitle}}'?", - "delete-tenant-text": "Будьте обережні, після підтвердження власник і всі пов'язані з ним дані стануть недоступними.", - "delete-tenants-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 власник} other {# власники} }?", - "delete-tenants-action-title": "Видалити { count, plural, 1 {1 власник} other {# власники} }", - "delete-tenants-text": "Будьте обережні, після підтвердження, усі вибрані власники будуть видалені, і всі пов'язані з ними дані стануть недоступними.", - "title": "Назва", - "title-required": "Необхідно вказати назву.", - "description": "Опис", - "details": "Деталі", - "events": "Події", - "copyId": "Крпіювати Id власника", - "idCopiedMessage": "Id власника скопійовано в буфер обміну", - "select-tenant": "Вибрати власника", - "no-tenants-matching": "Не знайдено жодних власників, які відповідають '{{entity}}'.", - "tenant-required": "Необхідно вказати власника", - "selected-tenants": "{ count, plural, 1 {1 власник} other {# власники} } вибрано", - "search": "Пошук власників" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 секунда} other {# секунди} }", - "minutes-interval": "{ minutes, plural, 1 {1 хвилина} other {# хвилини} }", - "hours-interval": "{ hours, plural, 1 {1 година} other {# години} }", - "days-interval": "{ days, plural, 1 {1 день} other {# дні} }", - "days": "Дні", - "hours": "Години", - "minutes": "Хвилини", - "seconds": "Секунди", - "advanced": "Додатково" - }, - "timewindow": { - "days": "{ days, plural, 1 { день } other {# дні } }", - "hours": "{ hours, plural, 0 { годин } 1 {1 година } other {# години } }", - "minutes": "{ minutes, plural, 0 { хвилин } 1 {1 хвилина } other {# хвилини } }", - "seconds": "{ seconds, plural, 0 { секунд } 1 {1 секунда } other {# секунди } }", - "realtime": "Реальний час", - "history": "Історія", - "last-prefix": "Останнє", - "period": "з {{ startTime }} до {{ endTime }}", - "edit": "Редагувати вікно часу", - "date-range": "Проміжок часу", - "last": "Останнє", - "time-period": "Період часу" - }, - "user": { - "user": "Користувач", - "users": "Користувачі", - "customer-users": "Користувачі клієнта", - "tenant-admins": "Адміністратори власників", - "sys-admin": "Системний адміністратор", - "tenant-admin": "Адміністратор власника", - "customer": "Клієнт", - "anonymous": "Анонім", - "add": "Додати користувача", - "delete": "Видалити користувача", - "add-user-text": "Додати нового користувача", - "no-users-text": "Не знайдено жодного користувача", - "user-details": "Подробиці про користувача", - "delete-user-title": "Ви впевнені, що хочете видалити користувача'{{userEmail}}'?", - "delete-user-text": "Будьте обережні, після підтвердження, користувач і всі пов'язані з ним дані стануть недоступними.", - "delete-users-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 користувача} other {# користувачів} }?", - "delete-users-action-title": "Видалити { count, plural, 1 {1 користувача} other {# користувачів} }", - "delete-users-text": "Будьте обережні, після підтвердження, усіх виділених користувачів буде видалено, і всі пов'язані з ними дані стануть недоступними.", - "activation-email-sent-message": "Повідомлення про активацію успішно надіслано!", - "resend-activation": "Повторно надіслати активацію", - "email": "Електронна пошта", - "email-required": "Необхідно вказати електронну пошту.", - "invalid-email-format": "Недійсний формат електронної пошти.", - "first-name": "Ім'я", - "last-name": "Прізвище", - "description": "Опис", - "default-dashboard": "Стандартна панель візуалізації", - "always-fullscreen": "Завжди в повноекранному режимі", - "select-user": "Вибрати користувача", - "no-users-matching": "Не знайдено жодного користувача, що відповідає '{{entity}}'.", - "user-required": "Необхідно вказати користувача", - "activation-method": "Спосіб активації", - "display-activation-link": "Показати посилання для активації", - "send-activation-mail": "Надіслати активаційного листа", - "activation-link": "Активаційне посилання для користувача", - "activation-link-text": "Для активувації користувача, скористайтеся наступним activation link :", - "copy-activation-link": "Скопіювати активаційне посилання ", - "activation-link-copied-message": "Посилання на активацію користувача було скопійовано в буфер обміну", - "selected-users": "{ count, plural, 1 {1 користувач} other {# користувачі} } вибрано", - "search": "Пошук користувачів", - "details": "Подробиці", - "login-as-tenant-admin": "Увійти як адміністратор власника", - "login-as-customer-user": "Увійти як користувач клієнта" - }, - "value": { - "type": "Тип значення", - "string": "Рядок", - "string-value": "Значення рядка", - "integer": "Ціле", - "integer-value": "Ціле значення", - "invalid-integer-value": "Недійсне ціле значення", - "double": "Подвійне", - "double-value": "Подвійне значення", - "boolean": "Логічне", - "boolean-value": "Логічне значення", - "false": "Помилкове", - "true": "Правдиве", - "long": "Довге" - }, - "widget": { - "widget-library": "Бібліотека віджетів", - "widget-bundle": "Пакет віджетів", - "select-widgets-bundle": "Виберіть пакет віджетів", - "management": "Керування віджетами", - "editor": "Редактор віджетів", - "widget-type-not-found": "Помилка завантаження конфігурації віджетів.
Можливо, пов'язаний з нею\n тип віджета було видалено.", - "widget-type-load-error": "Віджет не вдалося завантажити з наступних причин:", - "remove": "Видалити віджет", - "edit": "Відредагувати віджет", - "remove-widget-title": "Ви впевнені, що хочете видалити віджет '{{widgetTitle}}'?", - "remove-widget-text": "Після підтвердження віджет і всі пов'язані з ним дані стануть недоступними.", - "timeseries": "Телеметрія", - "search-data": "Пошук даних", - "no-data-found": "Даних не знайдено", - "latest-values": "Останні значення", - "rpc": "Керуючий віджет", - "alarm": "Віджет сигнала тривоги", - "static": "Статичний віджет", - "select-widget-type": "Вибрати тип віджета", - "missing-widget-title-error": "Необхідно вказати назву віджета!", - "widget-saved": "Віджет збережено", - "unable-to-save-widget-error": "Неможливо зберегти віджет! Віджет має помилки!", - "save": "Зберегти віджет", - "saveAs": "Зберегти віджет як", - "save-widget-type-as": "Зберегти тип віджета як", - "save-widget-type-as-text": "Введіть новий заголовок віджета та / або виберіть цільові віджети", - "toggle-fullscreen": "Перейти в повноекранний режим", - "run": "Запустити віджет", - "title": "Назва віджета", - "title-required": "Необхідно вказати назву віджета.", - "type": "Тип віджета", - "resources": "Ресурси", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Видалити ресурс", - "add-resource": "Додати ресурс", - "html": "HTML", - "tidy": "Форматувати", - "css": "CSS", - "settings-schema": "Схема налаштувань", - "datakey-settings-schema": "Схема налаштувань ключів даних", - "javascript": "Javascript", - "remove-widget-type-title": "Ви впевнені, що хочете видалити тип віджета '{{widgetName}}'?", - "remove-widget-type-text": "Будьте обережні, після підтвердження, тип віджета і всі пов'язані з ним дані стануть недоступними.", - "remove-widget-type": "Видалити тип віджета", - "add-widget-type": "Додати новий тип віджета", - "widget-type-load-failed-error": "Не вдалося завантажити тип віджета!", - "widget-template-load-failed-error": "Не вдалося завантажити шаблон віджета!", - "add": "Додати віджет", - "undo": "Скасувати зміни віджета", - "export": "Експртувати віджет", - "export-data": "Експортувати дані віджетів", - "export-to-csv": "Експортувати дані в CSV...", - "export-to-excel": "Експортувати дані в XLS..." - }, - "widget-action": { - "header-button": "Кнопка заголовка віджета", - "open-dashboard-state": "Перейти до нового стану панелі візуалізації", - "update-dashboard-state": "Оновити поточний стан панелі візуалізації", - "open-dashboard": "Перейти до іншої панелі візуалізації", - "custom": "Дії користувачів", - "custom-pretty": "Дії користувачів (з HTML шаблоном)", - "target-dashboard-state": "Цільовий стан панелі візуалізації", - "target-dashboard-state-required": "Необхідно вказати цільовий стан панелі візуалізації", - "set-entity-from-widget": "Встановити сутність із віджета", - "target-dashboard": "Цільова панель візуалізації", - "open-right-layout": "Відкрити мобільний режим панелі візуалізації" - }, - "widgets-bundle": { - "current": "Поточний зв'язок", - "widgets-bundles": "Пакети віджетів", - "add": "Додати пакет віджетів", - "delete": "Видалити пакет віджетів", - "title": "Назва", - "title-required": "Необхідно вказати назву віджета.", - "add-widgets-bundle-text": "Додати новий пакет віджетів", - "no-widgets-bundles-text": "Не знайдено жодних пакетів віджетів", - "empty": "Пакет віджетів порожній", - "details": "Подробиці", - "widgets-bundle-details": "Деталі пакетів віджетів", - "delete-widgets-bundle-title": "Ви впевнені, що хочете видалити пакет віджетів '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Будьте обережні, після підтвердження, пакети віджетів і всі пов'язані з ними дані стануть недоступними.", - "delete-widgets-bundles-title": "Ви впевнені, що хочете видалити { count, plural, 1 {пакет віджетів} other {# пакети віджетів} }?", - "delete-widgets-bundles-action-title": "Видалити { count, plural, 1 {1 пакет віджетів} other {# пакет віджетів} }", - "delete-widgets-bundles-text": "Будьте обережні, після підтвердження, всі виділені пакети віджетів і всі пов'язані з ними дані стануть недоступними.", - "no-widgets-bundles-matching": "Не знайдено жодних пакетів віджетів, які відповідають '{{widgetsBundle}}'.", - "widgets-bundle-required": "Необхідно вказати пакет віджетів.", - "system": "Системний", - "import": "Імпортувати пакет віджетів", - "export": "Експортувати пакет віджетів", - "export-failed-error": "Неможливо експортувати пакет віджетів: {{error}}", - "create-new-widgets-bundle": "Створити новий пакет віджетів", - "widgets-bundle-file": "Файл набору віджетів", - "invalid-widgets-bundle-file-error": "Неможливо імпортувати пакет віджетів: недійсна структура даних пакету віджетів." - }, - "widget-config": { - "data": "Дані", - "settings": "Налаштування", - "advanced": "Додатково", - "title": "Назва", - "general-settings": "Загальні налаштування", - "display-title": "Відобразити назву", - "drop-shadow": "Тінь", - "enable-fullscreen": "Увімкнути повноекранний режим", - "enable-data-export": "Увімкнути експорт даних", - "background-color": "Колір фону", - "text-color": "Колір тексту", - "padding": "Відступ", - "margin": "Границі", - "widget-style": "Стиль віджетів", - "title-style": "Стиль заголовка", - "mobile-mode-settings": "Налаштування мобільного режиму", - "order": "Порядок", - "height": "Висота", - "units": "Спеціальний символ після значення", - "decimals": "Кількість цифр після коми", - "timewindow": "Вікно часу", - "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", - "display-legend": "Показати легенду", - "datasources": "Джерела даних", - "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", - "datasource-type": "Тип", - "datasource-parameters": "Параметри", - "remove-datasource": "Видалити джерело даних", - "add-datasource": "Додати джерело даних", - "target-device": "Цільовий пристрій", - "alarm-source": "Джерело сигнала тривоги", - "actions": "Дії", - "action": "Дія", - "add-action": "Додати дію", - "search-actions": "Пошук дії", - "action-source": "Джерело дії", - "action-source-required": "Необхідно вказати джерело дії.", - "action-name": "Ім'я дії", - "action-name-required": "Необхідно вказати ім'я дії.", - "action-name-not-unique": "Дія з такою назвою вже існує.
Назва дії має бути унікальною в межах одного джерела дії.", - "action-icon": "Іконка", - "action-type": "Тип", - "action-type-required": "Необхідно вказати тип дії.", - "edit-action": "Редагувати дію", - "delete-action": "Видалити дію", - "delete-action-title": "Видалити дію віджета", - "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?" - }, - "widget-type": { - "import": "Імпортувати тип віджета", - "export": "Експортувати тип віджета", - "export-failed-error": "Неможливо експортувати тип віджета: {{error}}", - "create-new-widget-type": "Створити новий тип віджета", - "widget-type-file": "Файл типу віджета", - "invalid-widget-type-file-error": "Неможливо імпортувати тип віджету: неправильна структура даних типу віджета." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Нд", - "Mon": "Пн", - "Tue": "Вт", - "Wed": "Ср", - "Thu": "Чт", - "Fri": "Пт", - "Sat": "Сб", - "Jan": "Січ.", - "Feb": "Лют.", - "Mar": "Берез.", - "Apr": "Квіт.", - "May": "Трав.", - "Jun": "Черв.", - "Jul": "Лип.", - "Aug": "Серп.", - "Sep": "Верес.", - "Oct": "Жовт.", - "Nov": "Листоп.", - "Dec": "Груд.", - "January": "Січень", - "February": "Лютий", - "March": "Березень", - "April": "Квітень", - "June": "Червень", - "July": "Липень", - "August": "Серпень", - "September": "Вересень", - "October": "Жовтень", - "November": "Листопад", - "December": "Грудень", - "Custom Date Range": "Користувацький діапазон дат", - "Date Range Template": "Шаблон діапазону дат", - "Today": "Сьогодні", - "Yesterday": "Вчора", - "This Week": "Цього тижня", - "Last Week": "Минулий тиждень", - "This Month": "Цей місяць", - "Last Month": "Минулий місяць", - "Year": "Рік", - "This Year": "Цього року", - "Last Year": "Минулий рік", - "Date picker": "Вибір дати", - "Hour": "Година", - "Day": "День", - "Week": "Тиждень", - "2 weeks": "2 Тижні", - "Month": "Місяць", - "3 months": "3 Місяці", - "6 months": "6 Місяців", - "Custom interval": "Користувацький інтервал", - "Interval": "Інтервал", - "Step size": "Розмір кроку", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Атрибут не може бути вибраний в цьому віджеті", - "discard-changes": "Скасувати зміни", - "entity-attribute-required": "Значення атрибута обов'язкове", - "entity-timeseries-required": "Значення телеметрії обов'язкове", - "not-allowed-entity": "Обрана сутність не має спільних атрибутів", - "no-attribute-selected": "Атрибут не вибрано", - "no-entity-selected": "Сутність не вибрано", - "no-timeseries-selected": "Параметр телеметрії не вибрано", - "switch-attribute-value": "Змінити значення атрибута", - "switch-timeseries-value": "Змінити значення телеметрії", - "timeseries-not-allowed": "Телеметрія не може бути вибрана в цьому віджеті", - "update-failed": "Не вдалося оновити", - "update-successful": "Успішно оновлено", - "update-attribute": "Оновити атрибут", - "update-timeseries": "Оновити телеметрію", - "value": "Значення" - } - }, - "white-labeling": { - "white-labeling": "Білий маркування", - "login-white-labeling": "Login White Labeling", - "preview": "Попередній перегляд", - "app-title": "Назва програми", - "favicon": "Іконка веб-сайту", - "favicon-description": "*.ico, *.gif or *.png image with maximum size {{kbSize}} KBytes.", - "favicon-size-error": "Зображення веб-сайту завелике. Максимально дозволений розмір зображення веб-сайту {{kbSize}} KBytes.", - "favicon-type-error": "Недійсний формат файлу зображення веб-сайту. Приймаються лише зображення ICO, GIF або PNG.", - "drop-favicon-image": "Зніміть зображення піктограми веб-сайту або клацніть, щоб вибрати файл для завантаження.", - "no-favicon-image": "Не вибрано жодної іконки", - "logo": "Логотип", - "logo-description": "Будь-яке зображення з максимальним розміром {{kbSize}} KBytes.", - "logo-size-error": "Зображення логотипу занадто велике. Максимально дозволений розмір зображення логотипу{{kbSize}} KBytes.", - "logo-type-error": "Недійсний формат файлу логотипу. Приймаються тільки зображення.", - "drop-logo-image": "Зніміть зображення логотипу або клацніть, щоб вибрати файл для завантаження.", - "no-logo-image": "Не вибрано жожного логотипу", - "logo-height": "Висота логотипу, px", - "primary-palette": "Основна палітра", - "accent-palette": "Палітра акцент", - "customize-palette": "Налаштування", - "edit-palette": "Редагувати палітру", - "save-palette": "Зберегти палітру", - "primary-background": "Первинний фон", - "secondary-background": "Вторинний фон", - "hue1": "HUE 1", - "hue2": "HUE 2", - "hue3": "HUE 3", - "page-background-color": "Колір фону сторінки", - "dark-foreground": "Темний передній план", - "domain-name": "Доменне ім'я" - }, - "icon": { - "icon": "веб-іконка", - "select-icon": "Виберіть веб-іконку", - "material-icons": "Матеріал веб-іконки", - "show-all": "Показати всі веб-іконки" - }, - "custom": { - "widget-action": { - "action-cell-button": "Кнопка дії клітинки", - "row-click": "Клацніть на рядок", - "marker-click": "Клацніть на маркер", - "tooltip-tag-action": "Дії при підказці" - } - }, - "language": { - "language": "Мова", - "locales": { - "fr_FR": "Французька", - "zh_CN": "Китайська", - "en_US": "Англійська", - "it_IT": "Італійська", - "ko_KR": "Корейська", - "ru_RU": "Російська", - "es_ES": "Іспанська", - "ja_JA": "Японська", - "tr_TR": "Турецька", - "de_DE": "Німецька", - "uk_UA": "Українська", - "fa_IR": "Перська", - "cs_CZ": "Чеська" - } - } -} +{ + "access": { + "unauthorized": "Неавторизований", + "unauthorized-access": "Неавторизований доступ", + "unauthorized-access-text": "Щоб отримати доступ до цього ресурсу, потрібно ввійти в систему!", + "access-forbidden": "Доступ заборонено", + "access-forbidden-text": "Недостатньо прав для доступу!
Спробуйте увійти як інший користувач, якщо ви все ще хочете отримати доступ до цього ресурсу.", + "refresh-token-expired": "Дані про сесію застарілі", + "refresh-token-failed": "Не вдається відновити сеанс" + }, + "action": { + "activate": "Активувати", + "suspend": "Призупинити", + "save": "Зберегти", + "saveAs": "Зберегти як", + "cancel": "Скасувати", + "ok": "OK", + "delete": "Видалити", + "add": "Додати", + "yes": "Так", + "no": "Ні", + "update": "Оновити", + "remove": "Видалити", + "search": "Пошук", + "clear-search": "Очистити пошук", + "assign": "Надати", + "unassign": "Позбавити", + "share": "Поділитися", + "make-private": "Зробити приватним", + "apply": "Застосувати", + "apply-changes": "Застосувати зміни", + "edit-mode": "Режим редагування", + "enter-edit-mode": "Ввійти в режим редагування", + "decline-changes": "Відхилити зміни", + "close": "Закрити", + "back": "Назад", + "run": "Запустити", + "sign-in": "Увійти!", + "edit": "Редагувати", + "view": "Переглянути", + "create": "Створити", + "drag": "Перетягнути", + "refresh": "Оновити", + "undo": "Скасувати", + "copy": "Скопіювати", + "paste": "Вставити", + "copy-reference": "Копіювати посилання", + "paste-reference": "Вставити посилання", + "import": "Імпортувати", + "export": "Експортувати", + "share-via": "Поділитися через {{provider}}", + "continue": "Продовжити", + "discard-changes": "Скасувати зміни", + "move": "Перемістити", + "select": "Вибрати" + }, + "aggregation": { + "aggregation": "Агрегація", + "function": "Функція агрегації даних", + "limit": "Максимальні значення", + "group-interval": "Інтервал групування", + "min": "Мінімальний", + "max": "Максимальний", + "avg": "Середній", + "sum": "Сума", + "count": "Рахувати", + "none": "Відсутня" + }, + "admin": { + "general": "Загальне", + "general-settings": "Загальні налаштування", + "outgoing-mail": "Поштовий сервер", + "outgoing-mail-settings": "Налаштування сервера вихідної пошти", + "system-settings": "Налаштування системи", + "test-mail-sent": "Тестовий лист успішно відправлено!", + "base-url": "Базова URL-адреса", + "base-url-required": "Базова URL-адреса обов'язкова.", + "mail-from": "Електронна адреса", + "mail-from-required": "Електронна адреса обов'язкова.", + "smtp-protocol": "Протокол SMTP", + "smtp-host": "Хост SMTP", + "smtp-host-required": "Хост SMTP обов'язковий.", + "smtp-port": "SMTP-порт", + "smtp-port-required": "Ви повинні надати SMTP-порт.", + "smtp-port-invalid": "Це не схоже на дійсний SMTP-порт.", + "timeout-msec": "Час очікування (msec)", + "timeout-required": "Необхідно задати час очікування.", + "timeout-invalid": "Це не схоже на правильний час очікування.", + "enable-tls": "Увімкнути TLS", + "send-test-mail": "Надіслати тестове повідомлення", + "use-system-mail-settings": "Використовувати параметри системного поштового сервера", + "mail-templates": "Шаблони електронної пошти", + "mail-template-settings": "Налаштування шаблонів електронної пошти", + "use-system-mail-template-settings": "Використовувати шаблони системної електронної пошти", + "mail-template": { + "mail-template": "Шаблон електронної пошти", + "test": "Тестове повідомлення", + "activation": "Повідомлення про активацію рахунку", + "account-activated": "Обліковий запис активовано", + "reset-password": "Відновити повідомлення пароля", + "password-was-reset": "Пароль було надіслано повідомленням" + }, + "mail-subject": "Тема повідомлення", + "mail-body": "Вміст повідомлення", + "security-settings": "Налаштування безпеки", + "password-policy": "Політика щодо паролів", + "minimum-password-length": "Мінімальна довжина пароля", + "minimum-password-length-required": "Потрібна мінімальна довжина пароля", + "minimum-password-length-range": "Мінімальна довжина пароля повинна бути в межах від 5 до 50", + "minimum-uppercase-letters": "Мінімальна кількість великих літер", + "minimum-uppercase-letters-range": "Мінімальна кількість великих літер не може бути негативною", + "minimum-lowercase-letters": "Мінімальна кількість малих літер", + "minimum-lowercase-letters-range": "Мінімальна кількість малих літер не може бути негативною", + "minimum-digits": "Мінімальна кількість цифр", + "minimum-digits-range": "Мінімальна кількість цифр не може бути негативною", + "minimum-special-characters": "Мінімальна кількість спеціальних символів", + "minimum-special-characters-range": "Мінімальна кількість спеціальних символів не може бути негативною", + "password-expiration-period-days": "Термін дії пароля в днях", + "password-expiration-period-days-range": "Термін дії пароля в днях не може бути негативним", + "password-reuse-frequency-days": "Частота повторного використання пароля в днях", + "password-reuse-frequency-days-range": "Частота повторного використання пароля в днях не може бути негативною", + "general-policy": "Загальна політика", + "max-failed-login-attempts": "Максимальна кількість невдалих спроб входу, перш ніж обліковий запис заблоковано", + "minimum-max-failed-login-attempts-range": "Максимальна кількість невдалих спроб входу не може бути негативною", + "user-lockout-notification-email": "У разі блокування облікового запису користувача, надішліть сповіщення на електронну пошту" + }, + "alarm": { + "alarm": "Сигнал тривоги", + "alarms": "Сигнали тривоги", + "select-alarm": "Вибрати сигнал тривоги", + "no-alarms-matching": "Сигналів тривоги '{{entity}}' не знайдено.", + "alarm-required": "Сигнал тривоги необхідний", + "alarm-status": "Статус сигналу тривоги", + "search-status": { + "ANY": "Будь які", + "ACTIVE": "Активні", + "CLEARED": "Неактивні", + "ACK": "Прийняті", + "UNACK": "Неприйняті" + }, + "display-status": { + "ACTIVE_UNACK": "Активні та неприйняті", + "ACTIVE_ACK": "Активні та прийняті", + "CLEARED_UNACK": "Неактивні та неприйняті", + "CLEARED_ACK": "Неактивні та прийняті" + }, + "no-alarms-prompt": "Сигналів тривоги не знайдено", + "created-time": "Час створення", + "type": "Тип", + "severity": "Серйозність", + "originator": "Ініціатор", + "originator-type": "Тип ініціатору", + "details": "Деталі", + "status": "Статус", + "alarm-details": "Деталі сигналу тривоги", + "start-time": "Початок", + "end-time": "Кінець", + "ack-time": "Час прийняття", + "clear-time": "Час деактивації", + "severity-critical": "Критичні", + "severity-major": "Важливі", + "severity-minor": "Неважливі", + "severity-warning": "Попередження", + "severity-indeterminate": "Невизначені", + "acknowledge": "Прийняти", + "clear": "Деактивувати", + "search": "Шукати сигнали тривоги", + "selected-alarms": "{ count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} } вибрані", + "no-data": "Немає даних для відображення", + "polling-interval": "Інтервал опитування (сек)", + "polling-interval-required": "Необхідно задати інтервал опитування.", + "min-polling-interval-message": "Дозволяється щонайменше 1 секунда інтервалу очікування.", + "aknowledge-alarms-title": "Підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", + "aknowledge-alarms-text": "Ви впевнені, що хочете підтвердити { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", + "aknowledge-alarm-title": "Підтвердити сигнал тривоги", + "aknowledge-alarm-text": "Ви впевнені, що хочете підтвердити сигнал тривоги?", + "clear-alarms-title": "Деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }", + "clear-alarms-text": "Ви впевнені, що хочете деактивувати { count, plural, 1 {1 сигнал тривоги} other {# сигнали тривоги} }?", + "clear-alarm-title": "Деактивувати сигнал тривоги", + "clear-alarm-text": "Ви впевнені, що хочете деактивувати сигнал тривоги?", + "alarm-status-filter": "Фільтр статусу сигналу тривоги" + }, + "alias": { + "add": "Додати псевдонім ", + "edit": "Редагувати псевдонім", + "name": "Ім'я", + "name-required": "Необхідно вказати псевдонім", + "duplicate-alias": "Псевдонім з такою назвою вже існує.", + "filter-type-single-entity": "Єдина сутність", + "filter-type-entity-group": "Група сутностей", + "filter-type-entity-list": "Список сутностей", + "filter-type-entity-name": "Назва сутності", + "filter-type-entity-group-list": "Список груп сутностей", + "filter-type-entity-group-name": "Назва групи сутностей", + "filter-type-state-entity": "Сутність з стану панелі пристроїв", + "filter-type-state-entity-description": "Сутність, взята з параметрів стану панелі пристроїв", + "filter-type-asset-type": "Тип активу", + "filter-type-asset-type-description": "Тип активів '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Тип активів '{{assetType}}' і ім'я, що починаються з '{{prefix}}'", + "filter-type-device-type": "Тип пристрою", + "filter-type-device-type-description": "Тип пристроїв '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Тип пристроїв '{{deviceType}}' і ім'я, що починаються з '{{prefix}}'", + "filter-type-entity-view-type": "Тип перегляду сутності", + "filter-type-entity-view-type-description": "Перегляд сутності з типом '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Перегляд сутності з типом'{{entityView}}' і іменем, що починаються з '{{prefix}}'", + "filter-type-relations-query": "Запит відносин", + "filter-type-relations-query-description": "{{entities}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Запит пошуку активу", + "filter-type-asset-search-query-description": "Активи з типами {{assetTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Запит пошуку пристрою", + "filter-type-device-search-query-description": "Пристрої з типами {{deviceTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Запит пошуку переглядів сутностей", + "filter-type-entity-view-search-query-description": "Перегляд сутності з типами {{entityViewTypes}}, які мають {{relationType}} відношення {{direction}} {{rootEntity}}", + "entity-filter": "Фільтр сутності", + "resolve-multiple": "Як декілька сутностей", + "filter-type": "Тип фільтра", + "filter-type-required": "Необхідно вказати тип фільтра.", + "entity-filter-no-entity-matched": "Не знайдено жодних сутностей, які відповідають вказаному фільтру.", + "no-entity-filter-specified": "Фільтр обїектів не вказано", + "root-state-entity": "Використовувати сутінсть стану як кореневу", + "group-state-entity": "Використовувати групу сутностей стану як кореневу", + "root-entity": "Коренева сутність", + "state-entity-parameter-name": "Параметр сутності стану", + "default-state-entity": "Сутність стану за замовчуванням", + "default-state-entity-group": "Група сутностей стану за замовчуванням", + "default-entity-parameter-name": "За замовчуванням", + "max-relation-level": "Максимальна глибина відносин", + "unlimited-level": "Необмежена глибина", + "state-entity": "Сутність стану панелі пристроїв", + "entities-of-group-state-entity": "Сутності із групи сутностей стану", + "all-entities": "Всі сутності", + "any-relation": "не вказано" + }, + "asset": { + "asset": "Актив", + "assets": "Активи", + "management": "Управління активами", + "view-assets": "Переглянути активи", + "add": "Додати активи", + "assign-to-customer": "Надати клієнту", + "assign-asset-to-customer": "Надати активи клієнту", + "assign-asset-to-customer-text": "Будь ласка, виберіть ресурси, призначені для клієнта", + "no-assets-text": "Не знайдено активів", + "assign-to-customer-text": "Будь ласка, виберіть клієнта, щоб надати активи", + "public": "Публічно", + "assignedToCustomer": "Наданий клієнту", + "make-public": "Зробити актив(и) публічним(и)", + "make-private": "Зробити актив(и) приватним(и)", + "unassign-from-customer": "Позбавити клієнта", + "delete": "Видалити актив", + "asset-public": "Актив є загальнодоступним", + "asset-type": "Тип активу", + "asset-type-required": "Тип активу обов'язковий.", + "select-asset-type": "Виберіть тип активу", + "enter-asset-type": "Введіть тип активу", + "any-asset": "Будь-який актив", + "no-asset-types-matching": "Не знайдено жодних активів, що відповідають даному типу '{{entitySubtype}}'.", + "asset-type-list-empty": "Не вибрано жодного типу активів.", + "asset-types": "Типи активів", + "name": "Ім'я", + "name-required": "Ім'я обов'язкове.", + "description": "Опис", + "type": "Тип", + "type-required": "Тип обов'язковий.", + "details": "Подробиці", + "events": "Події", + "add-asset-text": "Додати новий актив", + "asset-details": "Інформація про актив", + "assign-assets": "Надати активи", + "assign-assets-text": "Надати { count, plural, 1 {1 актив} other {# активи} } клієнту", + "delete-assets": "Видалити активи", + "unassign-assets": "Позбавити активів", + "unassign-assets-action-title": "Позбавити { count, plural, 1 {1 актив} other {# активи} } клієнта", + "assign-new-asset": "Надати новий актив", + "delete-asset-title": "Ви впевнені, що хочете видалити актив '{{assetName}}'?", + "delete-asset-text": "Будьте обережні, після підтвердження, актив і всі пов'язані з ним дані буде втрачено", + "delete-assets-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# активи} }?", + "delete-assets-action-title": "Видалити{ count, plural, 1 {1 актив} other {# активи} }", + "delete-assets-text": "Будьте обережні, після підтвердження всі вибрані об'єкти буде видалено, і всі пов'язані з ними дані буде втрачено.", + "make-public-asset-title": "Ви дійсно хочете, щоб актив '{{assetName}}' був загальнодоступним?", + "make-public-asset-text": "Після підтвердження, актив і всі його дані будуть доступними для інших.", + "make-private-asset-title": "Ви впевнені, що хочете зробити актив {{assetName}} приватним?", + "make-private-asset-text": "Після підтвердження, актив та всі його дані будуть приватними та не будуть доступні іншим.", + "unassign-asset-title": "Ви впевнені, що хочете позбавити активу '{{assetName}}'?", + "unassign-asset-text": "Після підтвердження клієнт буде позбавлений активу. Дані активу не будуть доступні клієнту.", + "unassign-asset": "Позбавити активу", + "unassign-assets-title": "Ви впевнені, що хочете позбавити активів { count, plural, 1 {1 актив} other {# активи} }?", + "unassign-assets-text": "Після підтвердження, клієнт буде позбавлений усіх вибраних активів. Дані активів не будуть доступні клієнту.", + "copyId": "Копіювати Id активу", + "idCopiedMessage": "Id активу був скопійований у буфер обміну", + "select-asset": "Виберіть актив", + "no-assets-matching": "Не знайдено жодних активів, що відповідають'{{entity}}'.", + "asset-required": "Необхідно задати актив", + "name-starts-with": "Назва активу починається з", + "selected-assets": "{ count, plural, 1 {1 актив} other {# активи} } selected", + "search": "Пошук активів", + "select-group-to-add": "Виберіть цільову групу, щоб додати вибрані активи", + "select-group-to-move": "Виберіть цільову групу для переміщення вибраних активів", + "remove-assets-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# актив} } з групи '{entityGroup}'?", + "group": "Група активів", + "list-of-groups": "{ count, plural, 1 {Одна група активів} other {Список # груп активів} }", + "group-name-starts-with": "Групи активів, чиї імена починаються з '{{prefix}}'", + "import": "Імпортувати активи", + "asset-file": "Файл з активами", + "label": "Мітка" + }, + "attribute": { + "attributes": "Атрибути", + "latest-telemetry": "Остання телеметрія", + "attributes-scope": "Область видимості атрибутів", + "scope-latest-telemetry": "Остання телеметрія", + "scope-client": "Клієнтські атрибути", + "scope-server": "Серверні атрибути", + "scope-shared": "Спільні атрибути", + "add": "Додати атрибут", + "add-attribute-prompt": "Будь ласка, додайте атрибут", + "key": "Ключ", + "last-update-time": "Останнє оновлення", + "key-required": "Ключ атрибута обов'язковий.", + "value": "Значення", + "value-required": "Значення атрибута обов'язкове.", + "delete-attributes-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 attribute} other {# attributes} }?", + "delete-attributes-text": "Будьте обережні, після підтвердження, всі виділені атрибути будуть видалені.", + "delete-attributes": "Видалити атрибути", + "enter-attribute-value": "Введіть значення атрибута", + "show-on-widget": "Показати на віджеті", + "widget-mode": "Режим віджетів", + "next-widget": "Наступний віджет", + "prev-widget": "Попередній віджет", + "add-to-dashboard": "Додати до інформаційної панелі", + "add-widget-to-dashboard": "Додати віджет до інформаційної панелі", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected ...вибрані вибрати", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected" + }, + "audit-log": { + "audit": "Операція", + "audit-logs": "Журнал операцій", + "timestamp": "Тимчасова позначка", + "entity-type": "Тип одиниці", + "entity-name": "Назва організації", + "user": "Користувач", + "type": "Тип", + "status": "Статус", + "details": "Подробиці", + "type-added": "Додано", + "type-deleted": "Вилучено", + "type-updated": "Оновлено", + "type-attributes-updated": "Атрибути оновлені", + "type-attributes-deleted": "Атрибути видалені", + "type-rpc-call": "RPC дзвінок", + "type-credentials-updated": "Авторизаційні дані оновлено", + "type-assigned-to-customer": "Призначено клієнту", + "type-unassigned-from-customer": "Позбавлено від клієнта", + "type-activated": "Активовано", + "type-suspended": "Призупинено", + "type-credentials-read": "Авторизаційні дані прочитані", + "type-attributes-read": "Атрибути читаються", + "type-added-to-entity-group": "Додано до групи", + "type-removed-from-entity-group": "Вилучено з групи", + "type-relation-add-or-update": "Відношення оновлено", + "type-relation-delete": "Відношення видалено", + "type-relations-delete": "Всі відношення видалено", + "type-alarm-ack": "Визнано", + "type-alarm-clear": "Очищено", + "type-login": "Вхід", + "type-logout": "Вихід", + "type-lockout": "Заблокований", + "type-rest-api-rule-engine-call": "Rule engine REST API call", + "status-success": "Успішно", + "status-failure": "Невдало", + "audit-log-details": "Подробиці журналу операцій", + "no-audit-logs-prompt": "Жодних журналів операцій не знайдено", + "action-data": "Дані про дії", + "failure-details": "Невдалі подробиці", + "search": "Пошук журналів перевірки", + "clear-search": "Очистити пошук" + }, + "confirm-on-exit": { + "message": "У вас є незбережені зміни. Ви впевнені, що хочете залишити цю сторінку?", + "html-message": "У вас є незбережені зміни.
Ви впевнені, що хочете залишити цю сторінку?", + "title": "Незбережені зміни" + }, + "contact": { + "country": "Країна", + "city": "Місто", + "state": "Штат / Провінція", + "postal-code": "Поштовий індекс", + "postal-code-invalid": "Неправильний формат поштового індексу.", + "address": "Адреса", + "address2": "Адреса 2", + "phone": "Телефон", + "email": "Електронна пошта", + "no-address": "Немає адреси" + }, + "common": { + "username": "Ім'я користувача", + "password": "Пароль", + "enter-username": "Введіть ім'я користувача", + "enter-password": "Введіть пароль", + "enter-search": "Введіть пошук" + }, + "converter": { + "converter": "Перетворювач даних", + "converters": "Перетворювачі даних", + "select-converter": "Виберіть перетворювач даних", + "no-converters-matching": "Не має перетворювачів даних, які відповідають '{{entity}}'.", + "converter-required": "Необхідно вказати перетворювач даних", + "delete": "Видалити перетворювач даних", + "management": "Управління перетворювачами даних", + "add-converter-text": "Додати новий перетворювач даних", + "no-converters-text": "Перетворювачів даних не знайдено", + "selected-converters": "{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} } вибраний", + "delete-converter-title": "Ви впевнені, що хочете видалити перетворювач даних '{{converterName}}'?", + "delete-converter-text": "Будьте обережні, після підтвердження, перетворювач даних та всі пов'язані з ним дані,стануть недоступними).", + "delete-converters-title": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }?", + "delete-converters-action-title": "Видалити { count, plural, 1 {1 перетворювач даних} other {# перетворювачі даних} }", + "delete-converters-text": "Будьте обережні, після підтвердження всі вибрані перетворювачі даних буде видалено, і всі пов'язані з ними дані буде втрачено.", + "events": "Події", + "add": "Додати перетворювач даних", + "converter-details": "Подробиці про перетворювач даних", + "details": "Подробиці", + "copyId": "Копіювати Id перетворювача даних", + "idCopiedMessage": "Id перетворювача даних було скопійовано у буфер обміну", + "debug-mode": "Режим налагодження", + "name": "Ім'я", + "name-required": "Ім'я обов'язкове.", + "description": "Опис", + "decoder": "Декодер", + "encoder": "Кодер", + "test-decoder-fuction": "Тестування функції декодера", + "test-encoder-fuction": "Тестування функції кодера", + "decoder-input-params": "Вхідні параметри декодера", + "encoder-input-params": "Вхідні параметри кодера", + "payload": "Вхідне повідомлення", + "payload-content-type": "Тип контенту вхідного повідомлення", + "payload-content": "Зміст вхідного повідомлення", + "message": "Повідомлення", + "message-type": "Тип повідомлення", + "message-type-required": "Необхідно задати тип повідомлення", + "test": "Тест", + "metadata": "Метадані", + "metadata-required": "Записи метаданих не можуть бути порожніми.", + "integration-metadata": "Метедані інтеграції", + "integration-metadata-required": "Параметри метаданих інтеграції не можуть бути порожніми.", + "output": "Вихідні дані", + "import": "Імпорт перетворювача даних", + "export": "Експорт перетворювача даних", + "export-failed-error": "Неможливо експортувати перетворювач даних: {{помилка}}", + "create-new-converter": "Створити новий перетворювач даних", + "converter-file": "Файл перетворювача даних(конвектер файл)", + "invalid-converter-file-error": "Неможливо імпортувати перетворювач даних: недійсна структура даних перетворювача.", + "type": "Тип", + "type-required": "Необхідно задати тип.", + "type-uplink": "Від пристрою", + "type-downlink": "До пристрою" + }, + "content-type": { + "json": "Json", + "text": "Текст", + "binary": "Бінарний (Base64)" + }, + "customer": { + "customer": "Клієнт", + "customers": "Клієнти", + "management": "Клієнтський менеджмент", + "dashboard": "Інформаційна панель клієнта", + "dashboards": "Інформаційні панелі клієнта", + "devices": "Пристрої клієнта", + "entity-views": "Представлення сутностей", + "assets": "Клієнтські активи", + "public-dashboards": "Публічні інформаційні панелі", + "public-devices": "Публічні пристрої", + "public-assets": "Публічні активи", + "public-entity-views": "Публічне представлення сутностей 440", + "add": "Додати клієнта", + "delete": "Видалити клієнта", + "manage-customer-users": "Керування користувачами клієнта", + "manage-customer-devices": "Керування пристроями клієнта", + "manage-customer-dashboards": "Керування інформаційними панелями клієнта", + "manage-public-devices": "Керувати загальнодоступними пристроями", + "manage-public-dashboards": "Керування загальнодоступними інформаційними панелями", + "manage-customer-assets": "Керування активами клієнта", + "manage-public-assets": "Керування загальнодоступними активами", + "add-customer-text": "Додати нового клієнта", + "no-customers-text": "Клієнтів не знайдено", + "customer-details": "Інформація про клієнта", + "delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?", + "delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.", + "delete-customers-title": "Ви впевнені, що хочете видалити {count, plural, 1 {1 клієнт}, інші {# клієнти}}?", + "delete-customers-action-title": "Видалити{ count, plural, 1 {1 клієнт} other {# клієнти} }", + "delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.", + "manage-users": "Керування користувачами", + "manage-assets": "Керування активами", + "manage-devices": "Керування пристроями", + "manage-dashboards": "Керування інформаційними панелями", + "title": "Назва", + "title-required": "Необхідно задати назву.", + "description": "Опис", + "details": "Подробиці", + "events": "Події", + "copyId": "Копіювати Id клієнта", + "idCopiedMessage": "Id клієнта було скопійовано в буфер обміну", + "select-customer": "Виберіть клієнта", + "no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.", + "customer-required": "Необхідно задати клієнта", + "selected-customers": "{ count, plural, 1 {1 клієнт} інші {# клієнти} } вибрано", + "search": "Пошук клієнтів", + "select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів", + "select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів", + "remove-customers-from-group": "Ви впевнені, що хочете видалити{ count, plural, 1 {1 клієнт} other {# клієнти} } з групи'{entityGroup}'?", + "group": "Група клієнтів", + "list-of-groups": "{ count, plural, 1 {Одна група клієнтів} other {Список # груп клієнтів} }", + "group-name-starts-with": "Групи клієнтів, імена яких починаються з '{{prefix}}'", + "select-default-customer": "Вибрати клієнта за замовчуванням", + "default-customer": "Клієнт за замовчуванням", + "default-customer-required": "Необхідно вказати клієнта за замовчуванням для налагодження панелі візуалізації на рівні замовника", + "allow-white-labeling": "Дозволити брендування" + }, + "custom-translation": { + "custom-translation": "Переклад для користувача", + "translation-map": "Карта перекладу", + "key": "Ключ перекладу", + "import": "Імпорт перекладу", + "export": "Експорт перекладу", + "export-data": "Дані про експорт перекладу", + "import-data": "Дані про імпорт перекладу", + "translation-file": "Файл перекладу", + "invalid-translation-file-error": "Неможливо імпортувати файл перекладу: недійсна структура даних перекладу.", + "custom-translation-hint": "Визначте індивідуальний переклад в JSON нижче. Цей JSON перезапише переклад за замовчуванням. Натисніть 'Завантажити файл перекладу', щоб отримати існуючий переклад. Ви також можете скористатись завантаженим файлом як посиланням на наявні пари параметрів перекладу ключ-значення.", + "download-locale-file": "Завантажити файл перекладу" + }, + "datetime": { + "date-from": "Дата від", + "time-from": "Час від", + "date-to": "Дата до", + "time-to": "Час до" + }, + "dashboard": { + "dashboard": "Панель приладів", + "dashboards": "Панелі приладів", + "management": "Управління панеллю приладів", + "view-dashboards": "Переглянути панелі приладів", + "add": "Додати панель приладів", + "assign-dashboard-to-customer": "Призначити панель(і) приладів замовнику", + "assign-dashboard-to-customer-text": "Будь ласка, виберіть панелі пристроїв, щоб призначити їх клієнту", + "assign-to-customer-text": "Виберіть клієнта, щоб призначити панелі пристроїв", + "assign-to-customer": "Призначити клієнту", + "unassign-from-customer": "Позбавити клієнта", + "make-public": "Зробити панель приладів публічною", + "make-private": "Зробити панель приладів приватною", + "manage-assigned-customers": "Керування призначеними клієнтами", + "assigned-customers": "Призначені клієнтам", + "assign-to-customers": "Призначити панелі приладів клієнтам", + "assign-to-customers-text": "Виберіть клієнтів для призначення панелей приладів", + "unassign-from-customers": "Позбавити клієнтів призначенних панелей приладів", + "unassign-from-customers-text": "Виберіть клієнтів для позбавлення їх призначених панелей приладів", + "no-dashboards-text": "Панелі приладів не знайдені", + "no-widgets": "Не налаштовано жодних віджетів", + "add-widget": "Додати новий віджет", + "title": "Назва", + "select-widget-title": "Вибрати віджет", + "select-widget-subtitle": "Список доступних типів віджетів", + "delete": "Видалити панель приладів", + "title-required": "Необхідно задати назву.", + "description": "Опис", + "details": "подробиці", + "dashboard-details": "Подробиці панелі приладів", + "add-dashboard-text": "Додати нову панель приладів", + "assign-dashboards": "Призначити панель приладів", + "assign-new-dashboard": "Призначити нову панель приладів", + "assign-dashboards-text": "Призначити { count, plural, 1 {1 панель приладів} other {# панелі приладів} } користувачам", + "unassign-dashboards-action-text": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", + "delete-dashboards": "Видалити панель приладів", + "unassign-dashboards": "Позбавити панелей приладів", + "unassign-dashboards-action-title": "Позбавити { count, plural, 1 {1 палелі приладів} other {# панелей приладів} } клієнтів", + "delete-dashboard-title": "Ви впевнені, що хочете видалити панель приладів '{{назва панелі приладів}}'?", + "delete-dashboard-text": "Будьте обережні, після підтвердження, панель приладів і всі пов'язані з нею дані стануть недоступними.", + "delete-dashboards-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }?", + "delete-dashboards-action-title": "Видалити { count, plural, 1 {1 панель приладів} other {# панелі приладів} }", + "delete-dashboards-text": "Будьте обережні, після підтвердження, всі вибрані панелі приладів буде видалено, і всі пов'язані з ними дані стануть недоступними.", + "unassign-dashboard-title": "Ви впевнені, що хочете позбавити панелі приладів '{{назва інформаційної панелі}}'?", + "unassign-dashboard-text": "Після підтвердження, клієнт буде позбавлений панелі приладів. Панель приладів і пов'язані з нею дані будуть недоступні клієнтові.", + "unassign-dashboard": "Позбавити панелі приладів", + "unassign-dashboards-title": "Ви впевнені, що хочете позбавити { count, plural, 1 {1 панелі приладів} other {# панелей приладів} }?", + "unassign-dashboards-text": "Після підтвердження, клієтн буде позбавлений усіх вибраних панелей приладів і даних, які з ними пов'язані.", + "public-dashboard-title": "Панель приладів тепер публічна", + "public-dashboard-text": "Ваша панель приладів {{dashboardTitle}} тепер публічна і доступна іншим link:", + "public-dashboard-notice": "Note: Не забудьте зробити спільні пристрої загальнодоступними, щоб отримати доступ до їхніх даних.", + "make-private-dashboard-title": "Ви впевнені, що хочете зробити панель приладів '{{назва панелі приладів}}' приватною?", + "make-private-dashboard-text": "Після підтвердження панель приладів стане приватною і не буде доступною іншим.", + "make-private-dashboard": "Зробити панель приладів приватною", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Вибрати панель приладів", + "no-dashboards-matching": "Не знайдено жодних панелей прилодів'{{entity}}' які відповідають.", + "dashboard-required": "Необхідно задати панель приладів.", + "select-existing": "Виберіть існуючу панель приладів", + "create-new": "Створити нову панель приладів", + "new-dashboard-title": "Нова назва панелі приладів", + "open-dashboard": "Відрити панель приладів", + "set-background": "Встановити фон", + "background-color": "Колір фону", + "background-image": "Фонове зображення", + "background-size-mode": "Режим фонового розміру", + "no-image": "Не вибрано жодного зображення", + "drop-image": "Перетягніть зображення або клацніть, щоб вибрати файл для завантаження.", + "settings": "Налаштування", + "columns-count": "Кількість стовпців", + "columns-count-required": "Необхідно вказати кількість стовпців.", + "min-columns-count-message": "Дозволений мінімум -10 стовпців.", + "max-columns-count-message": "Дозволений максимум - 1000 стовпців.", + "widgets-margins": "Відступ між віджетами", + "horizontal-margin": "Горизонтальний відступ", + "horizontal-margin-required": "Необхідно вказати горизонтальний відступ.", + "min-horizontal-margin-message": "Допустиме мінімальне значення горизонтального відступу - 0.", + "max-horizontal-margin-message": "Допустиме максимальне значення горизонтального відступу - 50.", + "vertical-margin": "Вертикальний відступ", + "vertical-margin-required": "Необхідно вказати вертикальний відступ.", + "min-vertical-margin-message": "Допустиме мінімальне значення вертикального відступу - 0.", + "max-vertical-margin-message": "Допустиме максимальне значення вертикального відступу - 50.", + "autofill-height": "Висота автоматичного заповнення макета", + "mobile-layout": "Налаштування макета для мобільних пристроїв", + "mobile-row-height": "Висота рядка для мобільних пристроїв, px", + "mobile-row-height-required": "Потрібно вказати значення висоти рядка для мобільних пристроїв.", + "min-mobile-row-height-message": "Допустиме мінімальне значення висоти рядка для мобільних пристроїв - 5 пікселів.", + "max-mobile-row-height-message": "Допустиме максимальне значення висоти рядка для мобільних пристроїв - 200 пікселів.", + "display-title": "Відображати назву панелі візуалізації", + "toolbar-always-open": "Тримайте панель візуалізації відкритою", + "title-color": "Колір назви", + "display-dashboards-selection": "Відображення вибору панелей візуалізації", + "display-entities-selection": "Вибір відображення сутності", + "display-dashboard-timewindow": "Відобразити налаштування часового проміжку", + "display-dashboard-export": "Відображення експорту", + "import": "Імпортувати панель візуалізації", + "export": "Експортувати панель візуалізації", + "export-failed-error": "Неможливо експортувати панель візуалізації: {{error}}", + "export-pdf": "Експортувати як PDF", + "export-png": "Експортувати як PNG", + "export-jpg": "Експортувати як JPEG", + "export-json-config": "Експортувати конфігурацію JSON", + "download-dashboard-progress": "Генерування панелі візуалізації {{reportType}} ...", + "create-new-dashboard": "Створити нову панель візуалізації", + "dashboard-file": "Файл панелі візуалізації", + "invalid-dashboard-file-error": "Неможливо імпортувати панель візуалізації: неправильна структура даних панелі візуалізації.", + "dashboard-import-missing-aliases-title": "Configure aliases used by imported dashboard Налаштування псевдонімів, що використовуються імпортованою панеллю візуалізації", + "create-new-widget": "Створити новий віджет", + "import-widget": "Імпортувати віджет", + "widget-file": "Файл віджета", + "invalid-widget-file-error": "Неможливо імпортувати віджет: неправильна структура даних віджета.", + "widget-import-missing-aliases-title": "Налаштувати псевдоніми, що використовуються імпортованим віджетом", + "open-toolbar": "Відкрити панель інструменів ", + "close-toolbar": "Закрити панель інструменів", + "configuration-error": "Помилка конфігурації", + "alias-resolution-error-title": "Помилка конфігурації псевдонімів панелі візуалізації", + "invalid-aliases-config": "Неможливо знайти пристрої, які відповідають певному фільтру псевдонімів.
Зверніться до свого адміністратора, щоб вирішити цю проблему.", + "select-devices": "Вибрати пристрої", + "assignedToCustomer": "Призначений клієнту", + "assignedToCustomers": "Призначений клієнтам", + "public": "Публічно", + "public-link": "Публічне посилання", + "copy-public-link": "Копіювати публічне посилання", + "public-link-copied-message": "Публічне посилання було скопійоване в буфер обміну панелі візуалізації", + "manage-states": "Керування станами панелі візуалізації", + "states": "Стани панелі візуалізації", + "search-states": "Пошук станів панелі візуалізації", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } вибрано", + "edit-state": "Редагувати стан панелі візуалізації", + "delete-state": "Видалити стан панелі візуалізації ", + "add-state": "Додати стан панелі візуалізації", + "state": "Стан панелі візуалізації", + "state-name": "Ім'я", + "state-name-required": "Необхідно вказати назву стану панелі візуалізації.", + "state-id": "Стан Id", + "state-id-required": "Необхідно вказати id стану панелі візуалізації.", + "state-id-exists": "Стан інформаційної панелі з таким id вже існує.", + "is-root-state": "Основний стан", + "delete-state-title": "Видалити стан панелі візуалізації", + "delete-state-text": "Ви впевнені, що хочете видалити стан панелі візуалізації з іменем '{{stateName}}'?", + "show-details": "Показати деталі", + "hide-details": "Приховати деталі", + "select-state": "Виберіть цільовий стан", + "state-controller": "Контроллер стану" + }, + "datakey": { + "settings": "Налаштування", + "advanced": "Додатково", + "label": "Мітка", + "color": "Колір", + "units": "Спеціальний символ, який показує наступне значення", + "decimals": "Кількість цифр після плаваючої точки", + "data-generation-func": "Функція генерації даних", + "use-data-post-processing-func": "Використовувати функцію пост-обробки даних", + "configuration": "Конфігурація ключа даних", + "timeseries": "Телеметрія", + "attributes": "Атрибути", + "alarm": "Поля сигнала тривоги", + "timeseries-required": "Необхідно вказати Телеметрія.", + "timeseries-or-attributes-required": "Необхідно вказати телеметрію/атрибути.", + "maximum-timeseries-or-attributes": "Максимальні { count, plural, 1 {1 телеметрія/атрибут дозволені.} other {# телеметрія/атрибути дозволені} }", + "alarm-fields-required": "Необхідно вказати поля сигнала тривоги.", + "function-types": "Типи функцій", + "function-types-required": "Необхідно вказати типи функцій.", + "maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }", + "time-description": "мітка часу поточного значення;", + "value-description": "поточне значення;", + "prev-value-description": "результат попереднього виклику функції;", + "time-prev-description": "мітка часу попереднього значення;", + "prev-orig-value-description": "оригінальне попереднє значення;" + }, + "datasource": { + "type": "Тип джерела даних", + "name": "Ім'я", + "add-datasource-prompt": "Додайте джерело даних" + }, + "details": { + "details": "Деталі", + "edit-mode": "Режим редагування", + "toggle-edit-mode": "Перемкнути режим редагування" + }, + "device": { + "device": "Пристрій", + "device-required": "Необхідно задати пристрій.", + "devices": "Пристрої", + "management": "Управління пристроєм", + "view-devices": "Перегляд пристроїв", + "device-alias": "Псевдонім пристрою", + "aliases": "Псевдонім пристроїв", + "no-alias-matching": "'{{alias}}' не знайдено.", + "no-aliases-found": "Псевдонімів не знайдено.", + "no-key-matching": "'{{key}}' не знайдено.", + "no-keys-found": "Ключі не знайдено.", + "create-new-alias": "Створити новий!", + "create-new-key": "Створити новий!", + "duplicate-alias-error": "Псевдонім з таким іменем '{{alias}}' вже існює.
Псевдоніми пристроїв повинні бути унікальними на панелі візуалізації.", + "configure-alias": "Налаштувати псевдонім '{{alias}}'", + "no-devices-matching": "Не знайдено жодних пристроїв, які відповідають '{{entity}}'.", + "alias": "Псевдонім", + "alias-required": "Необхідно задати псевдонім пристрою.", + "remove-alias": "Видалити псевдонім пристрою", + "add-alias": "Додати псевдонім пристрою", + "name-starts-with": "Ім'я пристрою починається з", + "device-list": "Список пристроїв", + "use-device-name-filter": "Використати фільтр", + "device-list-empty": "Не вибрано жодного пристрою.", + "device-name-filter-required": "Необхідно задати назву фільтра пристрою.", + "device-name-filter-no-device-matched": "Не знайдено жодних пристроїв, що починаються з '{{device}}'.", + "add": "Додати пристрій", + "assign-to-customer": "Призначити клієнту", + "assign-device-to-customer": "Призначити пристрій (ої) клієнту", + "assign-device-to-customer-text": "Виберіть пристрої, які слід призначити клієнту", + "make-public": "Зробити пристрій публічним", + "make-private": "Зробити пристрій приватним", + "no-devices-text": "Не знайдено жодного пристрою", + "assign-to-customer-text": "Виберіть клієнта для призначення пристрою (їв)", + "device-details": "Деталі пристрою", + "add-device-text": "Додати новий пристрій", + "credentials": "Авторизаційні дані", + "manage-credentials": "Керування авторизаційними даними", + "delete": "Видалити пристрій", + "assign-devices": "Призначити пристрої", + "assign-devices-text": "Призначити { count, plural, 1 {1 пристрій} other {# пристрої} } клієнту", + "delete-devices": "Видалити пристрої", + "unassign-from-customer": "Позбавити клієнта пристроїв", + "unassign-devices": "Позбавити пристроїв", + "unassign-devices-action-title": "Позбавити клієнта { count, plural, 1 {1 пристрою} other {# пристроїв} }", + "assign-new-device": "Призначити новий пристрій", + "make-public-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' публічним?", + "make-public-device-text": "Після підтвердження пристрій і всі його дані будуть публічними та доступними для інших.", + "make-private-device-title": "Ви впевнені, що хочете зробити пристрій '{{deviceName}}' приватним?", + "make-private-device-text": "Після підтвердження пристрій і всі його дані будуть приватними та недоступними для інших.", + "view-credentials": "Переглянути авторизаційні дані", + "delete-device-title": "Ви впевнені, що хочете видалити пристрій '{{deviceName}}'?", + "delete-device-text": "Будьте обережні, після підтвердження, пристрій і всі пов'язані з ним дані стануть недоступними.", + "delete-devices-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} }?", + "delete-devices-action-title": "Видалити { count, plural, 1 {1 пристрій} other {# пристрої} }", + "delete-devices-text": "Будьте обережні, після підтвердження, всі вибрані пристрої будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "unassign-device-title": "Ви впевнені, що хочете позбавити пристрою '{{deviceName}}'?", + "unassign-device-text": "Після підтвердження, клієнт буде позбавлений пристрою.", + "unassign-device": "Позбавити пристою", + "unassign-devices-title": "Ви впевненені, що хочете позбавити { count, plural, 1 {1 пристрою} other {# пристроїв} }?", + "unassign-devices-text": "Після підтвердження, клієнт буде позбавлений пристрою і пристрій стане не доступним клієнту", + "device-credentials": "Авторизаційні дані прстрою", + "credentials-type": "Тип авторизаційних даних", + "access-token": "Маркер доступу", + "access-token-required": "Необхідно вказати маркер доступу.", + "access-token-invalid": "Маркер доступу має бути від 1 до 20 символів.", + "rsa-key": "Публічний ключ RSA", + "rsa-key-required": "Необхідно вказати публічний ключ RSA.", + "secret": "Секрет", + "secret-required": "Необхідно вказати секрет.", + "device-type": "Тип пристрою", + "device-type-required": "Необхідно вказати тип пристрою.", + "select-device-type": "Виберіть тип пристрою", + "enter-device-type": "Введіть тип пристрою", + "any-device": "Будь-який пристрій", + "no-device-types-matching": "Не знайдено типів пристроїв, які відповідають '{{entitySubtype}}'.", + "device-type-list-empty": "Не вибрано типів пристроїв.", + "device-types": "Типи пристрою", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "events": "Події", + "details": "Деталі", + "copyId": "Копіювати Id пристрою", + "copyAccessToken": "Копіювати маркер доступу", + "idCopiedMessage": "Id пристрою скопійовано в буфер обміну", + "accessTokenCopiedMessage": "Маркер доступу до пристрою скопійовано в буфер обміну", + "assignedToCustomer": "Призначений клієнту", + "unable-delete-device-alias-title": "Неможливо видалити псевдонім пристрою", + "unable-delete-device-alias-text": "Псевдонім пристрою '{{deviceAlias}}' не може бути видалений, оскільки він використовується наступним(и) віджетом(ами):
{{widgetsList}}", + "is-gateway": "Шлюз", + "public": "Публічно", + "device-public": "Пристрій є публічним", + "select-device": "Виберіть пристрій", + "import": "Імпортувати пристрої", + "device-file": "Файл з пристроями", + "selected-devices": "{ count, plural, 1 {1 пристрій} other {# пристрої} } вибрано", + "search": "Шукати пристрої", + "select-group-to-add": "Виберіть цільову групу, щоб додати вибраний пристрій", + "select-group-to-move": "Виберіть цільову групу для переміщення вибраних пристроїв", + "remove-devices-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 пристрій} other {# пристрої} } з групи '{entityGroup}'?", + "group": "Група пристроїв", + "list-of-groups": "{ count, plural, 1 {Одна група пристроїв} other {Список # груп пристроїв} }", + "group-name-starts-with": "Групи пристроїв, імена яких починаються з '{{prefix}}'" + }, + "dialog": { + "close": "Закрити діалогове вікно" + }, + "direction": { + "column": "Колонка", + "row": "Рядок" + }, + "error": { + "unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.", + "unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}", + "unknown-error": "Невідома помилка" + }, + "entity": { + "entity": "Сутність", + "entities": "Сутності", + "aliases": "Псевдоніми сутності", + "entity-alias": "Псевдонім сутності", + "unable-delete-entity-alias-title": "Неможливо видалити псевдонім сутності", + "unable-delete-entity-alias-text": "Псевдонім сутності'{{entityAlias}}' неможливо видалити, так як це використовується наступним віджетом(s):
{{widgetsList}}", + "duplicate-alias-error": "Знайдено повторюваний псевдонім '{{alias}}'.
Псевдоніми сутностей повинні бути унікальними на панелі візуалізації.", + "missing-entity-filter-error": "Відсутній фільтр для псевдоніма '{{alias}}'.", + "configure-alias": "Налаштувати '{{alias}}' псевдонім", + "alias": "Псевдонім", + "alias-required": "Необхідно вказати псевдонім сутності.", + "remove-alias": "Видалити псевдонім сутності", + "add-alias": "Додати псевдонім сутності", + "entity-list": "Список сутності", + "entity-type": "Тип сутності", + "entity-types": "Типи сутності", + "entity-type-list": "Список типу сутності", + "any-entity": "Будь-яка сутність", + "enter-entity-type": "Введіть тип сутності", + "no-entities-matching": "Не знайдено жожних сутностей, що відповідають '{{entity}}' що відповідають.", + "no-entity-types-matching": "Не знайдено жожних типів сутностей, що відповідають '{{entityType}}'.", + "name-starts-with": "Назва починається з", + "use-entity-name-filter": "Використовуйте фільтр", + "entity-list-empty": "Не вибрано жодних сутностей.", + "entity-type-list-empty": "Не вибрано жодних типів сутностей.", + "entity-name-filter-required": "Необхідно задати фільтр по імені.", + "entity-name-filter-no-entity-matched": "Не знайдено жодних сутностей, що починаються з '{{entity}}'.", + "all-subtypes": "Всі", + "select-entities": "Виберіть сутність", + "no-aliases-found": "Псевдонімів не знайдено.", + "no-alias-matching": "'{{alias}}' не знайдено.", + "create-new-alias": "Створити новий псевдонім!", + "key": "Ключ", + "key-name": "Ім'я ключа", + "no-keys-found": "No keys found.", + "no-key-matching": "'{{key}}' не знайдено.", + "create-new-key": "Створити новий ключ!", + "type": "Тип", + "type-required": "Необхідно задати тип сутності.", + "type-device": "Пристрій", + "type-devices": "Пристрої", + "list-of-devices": "{ count, plural, 1 {Один пристрій} other {Список # пристроїв} }", + "device-name-starts-with": "Пристрої, імена яких починаються з '{{prefix}}'", + "type-asset": "Актив", + "type-assets": "Активи", + "list-of-assets": "{ count, plural, 1 {Один актив} other {Список # активів} }", + "asset-name-starts-with": "Активи, імена яких починаються з '{{prefix}}'", + "type-entity-view": "Перегляд сутності", + "type-entity-views": "Перегляди сутності", + "list-of-entity-views": "{ count, plural, 1 {Один перегляд сутності} other {Список # переглядів сутності} }", + "entity-view-name-starts-with": "Перегляди сутностей, імена яких починаються з '{{prefix}}'", + "type-rule": "Правило", + "type-rules": "Правила", + "list-of-rules": "{ count, plural, 1 {Одне правило} other {Список # правил} }", + "rule-name-starts-with": "Правила, імена яких починаються з '{{prefix}}'", + "type-plugin": "Плагін", + "type-plugins": "Плагіни", + "list-of-plugins": "{ count, plural, 1 {Один плагін} other {Список # плагінів} }", + "plugin-name-starts-with": "Плагіни, імена яких починаються з '{{prefix}}'", + "type-tenant": "Власник", + "type-tenants": "Власники", + "list-of-tenants": "{ count, plural, 1 {Один власник} other {Список # власників} }", + "tenant-name-starts-with": "Власники, імена яких починаються з '{{prefix}}'", + "type-customer": "Клієнт", + "type-customers": "Клієнти", + "list-of-customers": "{ count, plural, 1 {Один клієнт} other {Список # клієнтів} }", + "customer-name-starts-with": "Клієнти, імена яких починаються з '{{prefix}}'", + "type-user": "Користувач", + "type-users": "Користувачі", + "list-of-users": "{ count, plural, 1 {Один користувач} other {Список # користувачів } }", + "user-name-starts-with": "Користувачі, імена яких починаються з '{{prefix}}'", + "type-dashboard": "Панель візуалізації", + "type-dashboards": "Панелі візуалізації", + "list-of-dashboards": "{ count, plural, 1 {Одна панель візуалізації} other {Список # панелей візуалізації} }", + "dashboard-name-starts-with": "Панелі візуалізації, імена яких починаються з '{{prefix}}'", + "type-alarm": "Сигнал тривоги", + "type-alarms": "Сигнали тривоги", + "list-of-alarms": "{ count, plural, 1 {Один сигнал тривоги} other {Список # сигналів тривоги} }", + "alarm-name-starts-with": "Сигнали тривоги, імена яких починаються '{{prefix}}'", + "type-rulechain": "Ланцюжок правил", + "type-rulechains": "Ланцюжки правил", + "list-of-rulechains": "{ count, plural, 1 {Один ланцюжок правил} other {Список # ланцюжків правил} }", + "rulechain-name-starts-with": "Правило ланцюжків, імена яких починаються '{{prefix}}'", + "type-scheduler-event": "Scheduler event", + "type-scheduler-events": "Scheduler events", + "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", + "scheduler-event-name-starts-with": "Scheduler events whose names start with '{{prefix}}'", + "type-blob-entity": "Blob entity", + "type-blob-entities": "Blob entities", + "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", + "blob-entity-name-starts-with": "Blob entities whose names start with '{{prefix}}'", + "type-rulenode": "Правило", + "type-rulenodes": "Правила", + "list-of-rulenodes": "{ count, plural, 1 {Одне правило} other {Список # правил} }", + "rulenode-name-starts-with": "Список правил, імена яких починаються '{{prefix}}'", + "type-current-customer": "Поточний клієнт", + "search": "Пошук сутностей", + "selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано", + "entity-name": "Ім'я сутності", + "entity-label": "Мітка сутності", + "details": "Подробиці сутності", + "no-entities-prompt": "Сутності не знайдено", + "no-data": "Немає даних для відображення", + "columns-to-display": "Стовпці для відображення", + "type-entity-group": "Група сутностей", + "type-converter": "Перетворювач даних", + "type-converters": "Перетворювачі даних", + "list-of-converters": "{ count, plural, 1 {Однин перетворювач даних} other {Список # перетворювачів даних} }", + "converter-name-starts-with": "Перетворювачі даних, імена яких починаються з '{{prefix}}'", + "type-integration": "Інтеграція", + "type-integrations": "Інтеграції", + "list-of-integrations": "{ count, plural, 1 {Одна інтеграція} other {Список # інтеграцій} }", + "integration-name-starts-with": "Інтеграції, імена яких починаються з '{{prefix}}'" + }, + "entity-group": { + "entity-group": "Група сутності", + "details": "Деталі", + "columns": "Стовпці", + "add-column": "Додати стовпець", + "column-value": "Значення", + "column-value-required": "Необхідно вказати значення.", + "column-title": "Назва", + "default-sort-order": "Основний порядок сортування", + "default-sort-order-required": "Необхідно вказати основний порядок сортування.", + "hide-in-mobile-view": "Мобільний приховано", + "use-cell-style-function": "Використовувати функцію стилю комірки", + "use-cell-content-function": "Use cell content function", + "edit-column": "Редагувати стовпець", + "column-details": "Деталі стовпця", + "actions": "Дії", + "settings": "Налаштування", + "delete": "Видалити групу сутностей", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "add": "Додати групу сутностей", + "add-entity-group-text": "Додати нову групу сутностей", + "no-entity-groups-text": "Не знайдено жодних груп сутності", + "entity-group-details": "Деталі групи сутності", + "delete-entity-groups": "Видалити групи сутностей", + "delete-entity-group-title": "Ви впевнені, що хочете видалити групу сутності '{{entityGroupName}}'?", + "delete-entity-group-text": "Будьте обережні, після підтвердження, група сутностей і всі пов'язані з нею дані стануть недоступними.", + "delete-entity-groups-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }?", + "delete-entity-groups-action-title": "Видалити { count, plural, 1 {1 групу сутності} other {# групи сутностей} }", + "delete-entity-groups-text": "Будьте обережні, після підтвердження, всі виділені групи сутностей і пов'язані з ними дані, стануть недоступними.", + "device-groups": "Групи пристроїв", + "asset-groups": "Групи активів", + "customer-groups": "Групи клієнтів", + "device-group": "Група пристроїв", + "asset-group": "Група активів", + "customer-group": "Група клієнтів", + "fetch-more": "Отримати більше", + "column-type": { + "column-type": "Тип стовпця", + "client-attribute": "Атрибут клієнта", + "shared-attribute": "Спільний атрибут", + "server-attribute": "Атрибут сервера", + "timeseries": "Телеметрія", + "entity-field": "Поле сутності" + }, + "column-type-required": "Необхідно вказати тип стовпця.", + "entity-field": { + "created-time": "Час створення", + "name": "Ім'я", + "type": "Тип", + "assigned_customer": "Призначений клієнт", + "authority": "Авторитет", + "first_name": "Ім'я", + "last_name": "Прізвище", + "email": "Електронна пошта", + "title": "Назва", + "country": "Країна", + "state": "Штат", + "city": "Місто", + "address": "Адреса", + "address2": "Адреса 2", + "zip": "Zip", + "phone": "Телефон" + }, + "sort-order": { + "asc": "У порядку зростання", + "desc": "У порядку зменшення", + "none": "Немає" + }, + "details-mode": { + "on-row-click": "Клацніть на рядок", + "on-action-button-click": "Клацніть на кнопку детелі", + "disabled": "Вимкнено" + }, + "add-to-group": "Додати до групи", + "move-to-group": "Перемістити до групи", + "select-entity-group": "Виберіть групу сутностей", + "no-entity-groups-matching": "Не знайдено жодних груп сутностей, що відповідають '{{entityGroup}}'.", + "target-entity-group-required": "Необхідно вказати цільову групу сутності.", + "remove-from-group": "Видалити з групи", + "group-table-title": "Group table title", + "enable-search": "Увімкнути пошук сутностей", + "enable-add": "Увімкнути додавання сутностей", + "enable-delete": "Увімкнути видалення сутностей", + "enable-selection": "Увімкнути вибір сутностей", + "enable-group-transfer": "Увімкнути дії групового перенесення", + "display-pagination": "Відображення сторінок", + "default-page-size": "Розмір сторінки за замовчуванням", + "enable-assignment-actions": "Увімкнути дії призначення", + "enable-credentials-management": "Увімкнути керування авторизаційними даними", + "enable-users-management": "Увімкнути керування користувачами", + "enable-assets-management": "Увімкнути керування активами", + "enable-devices-management": "Увімкнути керування пристроями", + "enable-dashboards-management": "Увімкнути керування панелями візуалізації", + "open-details-on": "Відкрити деталі сутності по", + "select-existing": "Виберіть існуючу групу сутностей", + "create-new": "Створити нову групу сутностей", + "new-entity-group-name": "Нове ім'я групи сутностей", + "entity-group-list": "Список групи сутностей", + "entity-group-list-empty": "Не вибрано жодної групи сутностей.", + "name-starts-with": "Назва групи сутностей починається з", + "entity-group-name-filter-required": "Необхідно задати назву групи сутностей." + }, + "entity-view": { + "entity-view": "Представлення сутності", + "entity-view-required": "Необхідно вказати представлення сутності.", + "entity-views": "Представлення сутностей", + "management": "Керування представленням сутностей", + "view-entity-views": "Переглянути представлення сутностей", + "entity-view-alias": "Псевдонім представлення сутності", + "aliases": "Псевдоніми представлення сутності", + "no-alias-matching": "Псевдонім'{{alias}}' не знайдено.", + "no-aliases-found": "Псевдоніми не знайдено.", + "no-key-matching": "'Ключ {{key}}' не знайдено.", + "no-keys-found": "Ключі не знайдено.", + "create-new-alias": "Створити новий!", + "create-new-key": "Створити новий!", + "duplicate-alias-error": "Псевдонім з такою назвою вже існує '{{alias}}'.
Псевдоніми представлення повинні бути унікальними на панелі візуалізації.", + "configure-alias": "Налаштувати псевдонім '{{alias}}'", + "no-entity-views-matching": "Сутності, які відповідають '{{entity}}' не знайдені.", + "alias": "Псевдонім", + "alias-required": "Необхідно вказати псевдонім представлення сутності.", + "remove-alias": "Видалити псевдонім представлення сутності", + "add-alias": "Додати псевдонім представлення сутності", + "name-starts-with": "Ім'я представлення сутності починається з", + "entity-view-list": "Список представленнь сутності", + "use-entity-view-name-filter": "Використати фільтр", + "entity-view-list-empty": "Не вибрано жодного представлення сутності.", + "entity-view-name-filter-required": "Необхідно вказвти фільтр назв представлення сутності.", + "entity-view-name-filter-no-entity-view-matched": "Представлення сутностей, назви яких починаються з '{{entityView}}' не знайдено.", + "add": "Додати представлення сутності", + "assign-to-customer": "Призначити клієнту", + "assign-entity-view-to-customer": "Призначити представлення сутності(ей) клієнту", + "assign-entity-view-to-customer-text": "Будь ласка, виберіть представлення сутності для призначення клієнту", + "no-entity-views-text": "Представлення сутності не знайдено", + "assign-to-customer-text": "Будь ласка, виберіть клієнта, для призначиення представлення(ь) сутності(ей)", + "entity-view-details": "Деталі представлення сутності", + "add-entity-view-text": "Додати нове представлення сутності", + "delete": "Видалити представлення сутності", + "assign-entity-views": "Призначити представлення сутності", + "assign-entity-views-text": "Призначити { count, plural, 1 {1 представлення сутності} other {# представлення сутностей } } клієнту", + "delete-entity-views": "Видалити представлення сутностей", + "unassign-from-customer": "Відкликати у клієнта", + "unassign-entity-views": "Відкликати представлення сутностей", + "unassign-entity-views-action-title": "Відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} } у клієнта", + "assign-new-entity-view": "Призначити нове представлення сутності", + "delete-entity-view-title": "Ви впевнені, що хочете видалити представлення сутності'{{entityViewName}}'?", + "delete-entity-view-text": "Будьте обережні, після підтвердження, представлення сутності та всі пов'язані з ним дані стануть недоступними.", + "delete-entity-views-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }?", + "delete-entity-views-action-title": "Видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }", + "delete-entity-views-text": "Будьте обережні, після підтвердження, всі виділені представлення сутностей та дні, пов'язані з ними стануть недоступними.", + "unassign-entity-view-title": "Ви впевнені, що хочете відкликати представлення сутності '{{entityViewName}}'?", + "unassign-entity-view-text": "Після підтвердження представлення сутності буде відкликане у клієнта. Дані представлення сутності не будуть доступні клієнту.", + "unassign-entity-view": "Відкликати представлення сутності", + "unassign-entity-views-title": "Ви впевнені, що хочете відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} }?", + "unassign-entity-views-text": "Після підтвердження, клієнта буде позбавлено всіх виділених представлень сутностей. Дані представлень сутностей не будуть доступні клієнту.", + "entity-view-type": "Тип представлення сутності", + "entity-view-type-required": "Необхідно вказати тип представлення сутності.", + "select-entity-view-type": "Виберіть тип представлення сутності", + "enter-entity-view-type": "Введіть тип представлення сутності", + "any-entity-view": "Будь-яке представлення сутності", + "no-entity-view-types-matching": "Не знайдено жодних типів представлення сутності, що відповідають '{{entitySubtype}}'.", + "entity-view-type-list-empty": "Не вибрано тип представлення сутності.", + "entity-view-types": "Типи представлення сутності", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "events": "Події", + "details": "Деталі", + "copyId": "Скопіювати Id представлення сутності", + "assignedToCustomer": "Призначений клієнту", + "unable-entity-view-device-alias-title": "Неможливо видалити псевдонім представлення сутності", + "unable-entity-view-device-alias-text": "Не вдалося видалити псевдонім пристрою'{{entityViewAlias}}', так як він використовується наступним(и) віджетом(ами):
{{widgetsList}}", + "select-entity-view": "Вибрати представлення сутності", + "make-public": "Зробити представлення сутності публічним", + "make-private": "Зробити представлення сутності приватним", + "start-date": "Дата початку", + "start-ts": "Час початку", + "end-date": "Дата закінчення", + "end-ts": "Час завершення", + "date-limits": "Обмеження дати", + "client-attributes": "Атрибути клієнта", + "shared-attributes": "Спільні атрибути", + "server-attributes": "Атрибути сервера", + "timeseries": "Телеметрія", + "client-attributes-placeholder": "Атрибути клієнта", + "shared-attributes-placeholder": "Спільні атрибути", + "server-attributes-placeholder": "Атрибути сервера", + "timeseries-placeholder": "Телеметрія", + "target-entity": "Цільова сутність", + "attributes-propagation": "Поширення атрибутів", + "attributes-propagation-hint": "Представлення сутності автоматично копіюватиме вказані атрибути з цільової сутності кожного разу, коли ви зберігаєте або оновлюєте його. В цілях продуктивності, атрибути цільової сутності не поширюються на представлення сутності при кожній зміні її атрибутів. Можна ввімкнути автоматичне поширення, налаштувавши правило \"copy to view\" у вашому ланцюжку правил і пов'язуючи його з повідомленнями типу \"Post attributes\" і \"Attributes Updated\"..", + "timeseries-data": "Дані телеметрії", + "timeseries-data-hint": "Налаштуйте ключі даних телеметрії цільової сутності, які будуть доступні представленню сутності. Ці дані доступні лише для читання.", + "make-public-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' публічним?", + "make-public-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть публічними і будуть доступні для інших користувачів.", + "make-private-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' приватним?", + "make-private-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть приватними і не будуть доступні для інших користувачів." + }, + "event": { + "events": "Події", + "event-type": "Тип події", + "type-error": "Помилка", + "type-lc-event": "Подія життєвого циклу", + "type-stats": "Статистика", + "type-debug-converter": "Налагоджувати", + "type-debug-integration": "Налагоджувати", + "type-debug-rule-node": "Налагоджувати", + "type-debug-rule-chain": "Налагоджувати", + "no-events-prompt": "Не знайдено жодних подій", + "error": "Помилка", + "alarm": "Сигнал тривоги", + "event-time": "Час події", + "server": "Сервер", + "body": "Тіло", + "method": "Метод", + "type": "Тип", + "in": "In", + "out": "Out", + "metadata": "Метадані", + "message": "Повідомлення", + "entity": "Сутність", + "message-id": "Id повідомлення", + "message-type": "Тип повідомлення", + "data-type": "Тип даних", + "relation-type": "Тип зв'язку", + "data": "Дані", + "event": "Подія", + "status": "Статус", + "success": "Успіх", + "failed": "Невдача", + "messages-processed": "Повідомлення опрацьовані", + "errors-occurred": "Виникли помилки" + }, + "extension": { + "extensions": "Розширення", + "selected-extensions": " вибрано { count, plural, 1 {1 розширення} other {# розширення} }", + "type": "Тип", + "key": "Ключ", + "value": "Значення", + "id": "Id", + "extension-id": "Id розширення", + "extension-type": "Тип розширення", + "transformer-json": "JSON *", + "unique-id-required": "Таке Id розширення вже існує.", + "delete": "Видалити розширення", + "add": "Додати розширення", + "edit": "Редагувати розширення", + "delete-extension-title": "Ви дійсно бажаєте видалити розширення '{{extensionId}}'?", + "delete-extension-text": "Будьте обережні, після підтвердження, розширення та всі пов'язані з ним дані стануть недоступними.", + "delete-extensions-title": "Ви дійсно бажаєте видалити { count, plural, 1 {1 розширення} other {# розширення} }?", + "delete-extensions-text": "Будьте обережні, після підтвердження, всі вибрані розширення будуть видалені.", + "converters": "Перетворювачі", + "converter-id": "Id перетворювача", + "configuration": "Конфігурація", + "converter-configurations": "Конфігурації перетворювача", + "token": "Маркер безпеки", + "add-converter": "Додати конвертер", + "add-config": "Додати конфігурацію конвертера", + "device-name-expression": "Маска імені пристрою", + "device-type-expression": "Маска типу пристрою", + "custom": "Користувач", + "to-double": "Подвоїти", + "transformer": "Трансформатор", + "json-required": "Необхідно вказати json трансформатора.", + "json-parse": "Неможливо проаналізувати json трансформатора.", + "attributes": "Атрибути", + "add-attribute": "Додати атрибут", + "add-map": "Додати елемент відображення", + "timeseries": "Телеметрія", + "add-timeseries": "Додати параметри телеметрії", + "field-required": "Field is required", + "brokers": "Брокери", + "add-broker": "Додати брокера", + "host": "Хост", + "port": "Порт", + "port-range": "Значення порту має бути в діапазоні від 1 до 65535.", + "ssl": "Ssl", + "credentials": "Авторизаційні дані", + "username": "Ім'я користувача", + "password": "Пароль", + "retry-interval": "Інтервал повтору в мілісекундах", + "anonymous": "Анонімний", + "basic": "Основний", + "pem": "PEM", + "ca-cert": "Файл CA сертифіката *", + "private-key": "Файл приватного ключа *", + "cert": "Файл сертифіката *", + "no-file": "Не вибрано жодного файлу.", + "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", + "mapping": "Зіставлення", + "topic-filter": "Фільтр тем", + "converter-type": "Тип конвертера", + "converter-json": "Json", + "json-name-expression": " Json вираз для назви пристрою", + "topic-name-expression": "Вираз для назви пристрою в назві теми", + "json-type-expression": "Json вираз для типу пристрою", + "topic-type-expression": "Вираз для типу пристрою в назві теми", + "attribute-key-expression": "Вираз для ключа атрибута", + "attr-json-key-expression": " Json вираз для ключа атрибута", + "attr-topic-key-expression": "Вираз для ключа атрибута в назві теми", + "request-id-expression": "Вираз для id запиту", + "request-id-json-expression": "Json вираз для id запиту", + "request-id-topic-expression": "Вираз для id запиту в назві теми", + "response-topic-expression": "Вираз для теми відповідей", + "value-expression": "Вираз для значення", + "topic": "Тема", + "timeout": "Час очікування в мілісекундах", + "converter-json-required": "Необхідно вказати json конвертер.", + "converter-json-parse": "Неможливо проаналізувати json конвертера.", + "filter-expression": "Вираз для фільтра", + "connect-requests": "Запити на підключення", + "add-connect-request": "Додати запит на підключення", + "disconnect-requests": "Відключення запитів", + "add-disconnect-request": "Додати запит на відключення", + "attribute-requests": "Запити атрибутів", + "add-attribute-request": "Додати запит атрибута", + "attribute-updates": "Оновлення атрибутів", + "add-attribute-update": "Додати оновлення атрибутів", + "server-side-rpc": "Серверна сторона RPC", + "add-server-side-rpc-request": "Додати RPC-запит на стороні сервера", + "device-name-filter": "Фільтр назви пристрою", + "attribute-filter": "Фільтр атрибутів", + "method-filter": "Фільтр методів", + "request-topic-expression": "Вираз для теми запитів", + "response-timeout": "Час очікування відповіді в мілісекундах", + "topic-expression": "Вираз для назви теми", + "client-scope": "Обсяг клієнта", + "add-device": "Додати пристрій", + "opc-server": "Сервери", + "opc-add-server": "Додати сервер", + "opc-add-server-prompt": "Будь ласка, додайте сервер", + "opc-application-name": "Назва програми", + "opc-application-uri": "URI програми", + "opc-scan-period-in-seconds": "Період сканування в секундах", + "opc-security": "Безпека", + "opc-identity": "Ідентифікація", + "opc-keystore": "Сховище ключів", + "opc-type": "Тип", + "opc-keystore-type": "Тип", + "opc-keystore-location": "Розташування *", + "opc-keystore-password": "Пароль", + "opc-keystore-alias": "Псевдонім", + "opc-keystore-key-password": "Пароль для ключа", + "opc-device-node-pattern": "Патерн OPC вузла пристрою", + "opc-device-name-pattern": "Патерн назви пристрою", + "modbus-server": "Сервери/ведені пристрої", + "modbus-add-server": "Додати сервер/ведений пристрій", + "modbus-add-server-prompt": "Будь ласка, додайте сервер/ведений пристрій", + "modbus-transport": "Транспорт", + "modbus-tcp-reconnect": "Перепідключатися автоматично", + "modbus-port-name": "Ім'я послідовного порту", + "modbus-encoding": "Кодування", + "modbus-parity": "Паритет", + "modbus-baudrate": "Швидкість передачі даних", + "modbus-databits": "Біти даних", + "modbus-stopbits": "Стоп-біти", + "modbus-databits-range": "Біти даних повинні знаходитися в діапазоні від 7 до 8.", + "modbus-stopbits-range": "Стоп-біти повинні знаходитися в діапазоні від 1 до 2.", + "modbus-unit-id": "Unit ID", + "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", + "modbus-device-name": "Ім'я пристрою", + "modbus-poll-period": "Період опитування (мс)", + "modbus-attributes-poll-period": "Період опитування атрибутів (мс)", + "modbus-timeseries-poll-period": "Період опитування телеметрії (мс)", + "modbus-poll-period-range": "Період опитування повинен бути більше 0.", + "modbus-tag": "Тег", + "modbus-function": "Modbus функція", + "modbus-register-address": "Адреса регістру ", + "modbus-register-address-range": "Адреса регістру повинна бути в діапазоні від 0 до 65535.", + "modbus-register-bit-index": "Номер бітів", + "modbus-register-bit-index-range": "Номер бітів повинен знаходитися в діапазоні від 0 до 15.", + "modbus-register-count": "Рахунок регістру", + "modbus-register-count-range": "Рахунок регістру повинен бути більше 0.", + "modbus-byte-order": "Порядок байтів", + "sync": { + "status": "Статус", + "sync": "Синхронізований", + "not-sync": "Не синхронізований", + "last-sync-time": "Час останньої синхронізації", + "not-available": "Недоступний" + }, + "export-extensions-configuration": "Експортувати конфігурацію розширень", + "import-extensions-configuration": "Імпортувати конфігурацію розширень", + "import-extensions": "Імпортувати розширення", + "import-extension": "Імпортувати розширення", + "export-extension": "Експортувати розширення", + "file": "Файл розширень", + "invalid-file-error": "Не правильний формат файла" + }, + "fullscreen": { + "expand": "Відкрити у повноекранному режимі", + "exit": "Вийти з повноекранного режиму", + "toggle": "Перемкнути повноекранний режим", + "fullscreen": "Повноекранний режим" + }, + "function": { + "function": "Функція" + }, + "grid": { + "delete-item-title": "Ви впенені, що хочете видалити цей елемент?", + "delete-item-text": "Будьте обережні, після підтвердження, цей елемент і всі пов'язані з ним дані, стануть недоступними.", + "delete-items-title": "Ви впенені, що хочете видалити { count, plural, 1 {1 елемент} other {# елементи} }?", + "delete-items-action-title": "Видалити{ count, plural, 1 {1 елемент} other {# елементи} }", + "delete-items-text": "Будьте обережні, після підтвердження, всі виділені елементи і пов'язані з ними дані, стануть недоступними.", + "add-item-text": "Додати новий елемент", + "no-items-text": "Не знайдено жодного елемента", + "item-details": "Деталі елемента", + "delete-item": "Видалити елемент", + "delete-items": "Видалити елементи", + "scroll-to-top": "Перейти угору" + }, + "help": { + "goto-help-page": "Перейти на сторінку довідки" + }, + "home": { + "home": "Домашня сторінка", + "profile": "Профіль", + "logout": "Вийти", + "menu": "Меню", + "avatar": "Аватар", + "open-user-menu": "Відкрити меню користувача" + }, + "import": { + "no-file": "Не вибрано жодного файлу", + "drop-file": "Перетягніть JSON файл, або клацніть, щоб вибрати файл для завантаження.", + "drop-csv-file": "Перетягніть CSV файл, або клацніть, щоб вибрати файл для завантаження.", + "column-value": "Значення", + "column-title": "Назва", + "column-example": "Приклад значень даних", + "column-key": "Ключ атрибута/телеметрії", + "csv-delimiter": "Розділювач в CSV файлі", + "csv-first-line-header": "Перший рядок містить назви колонок", + "csv-update-data": "Оновити атрибути/телеметрію", + "import-csv-number-columns-error": "Файл має містити як мінімум дві колонки", + "import-csv-invalid-format-error": "Невірний формат даних. Рядок: '{{line}}'", + "column-type": { + "name": "Назва", + "type": "Тип", + "label": "Мітка", + "column-type": "Тип колонки", + "client-attribute": "Атрибут клієнта", + "shared-attribute": "Спільний атрибут", + "server-attribute": "Атрибут сервера", + "timeseries": "Телеметрія", + "entity-field": "Entity field", + "access-token": "Токен" + }, + "stepper-text": { + "select-file": "Виберіть файл", + "configuration": "Конфігурація імпорту", + "column-type": "Виберіть тип колонок", + "creat-entities": "Створення нових сутностей", + "done": "Завершено" + }, + "message": { + "create-entities": "{{count}} нову(их) сутність(ей) успішно створено.", + "update-entities": "{{count}} сутність(ей) успішно оновлено.", + "error-entities": "Виникла помилка при створенні {{count}} сутності(ей)." + } + }, + "integration": { + "integration": "Інтеграція", + "integrations": "Інтеграції", + "select-integration": "Виберіть інтеграцію", + "no-integrations-matching": "Не знайдено жодних інтеграцій, які відповідають '{{entity}}'.", + "integration-required": "Необхідно вказати інтеграцію", + "delete": "Видалити інтеграцію", + "management": "Управління інтеграціями", + "add-integration-text": "Додати нову інтеграцію", + "no-integrations-text": "Не знайдено жодної інтеграції", + "selected-integrations": "{ count, plural, 1 {1 інтеграція} other {# інтеграції} } вибрано", + "delete-integration-title": "Ви впевнені, що хочете видалити інтеграцію '{{integrationName}}'?", + "delete-integration-text": "Будьте обережні, після підтвердження інтеграція та всі пов'язані з нею дані стануть недоступними.", + "delete-integrations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }?", + "delete-integrations-action-title": "Видалити { count, plural, 1 {1 інтеграцію} other {# інтеграції} }", + "delete-integrations-text": "Будьте обережні, після підтвердження всі вибрані інтеграції будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "events": "Події", + "add": "Додати інтеграцію", + "integration-details": "Деталі інтеграції", + "details": "Деталі", + "copyId": "Копіювати Id інтеграції", + "idCopiedMessage": "Id інтеграції скопійовано в буфер обміну", + "debug-mode": "Режим налагодження", + "enable-security": "Увімкнути безпеку", + "headers-filter": "Заголовки фільтра", + "header": "Заголовок", + "no-headers-filter": "Немає фільтрів заголовків", + "downlink-url": "Downlink URL", + "application-uri": "URI програми", + "as-id": "AS ID", + "as-id-required": "Необхідно вказати AS ID.", + "as-key": "AS ключ", + "as-key-required": "Необхідно вказати AS ключ.", + "max-time-diff-in-seconds": "Максимальна різниця в часі (секунди)", + "max-time-diff-in-seconds-required": "Необхідно вказати максимальну різницю в часі.", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "base-url": "Базова URL-адреса", + "base-url-required": "Необхідно вказати базову URL-адресу", + "security-key": "Ключ захисту", + "http-endpoint": "URL кінцевої точки HTTP", + "copy-http-endpoint-url": "Скопіювати URL-адресу кінцевої точки HTTP", + "http-endpoint-url-copied-message": "URL кінцевої точки HTTP скопійовано в буфер обміну", + "host": "Хост", + "host-required": "Необхідно вказати хост.", + "host-type": "Тип хоста", + "host-type-required": "Необхідно вказати тип хоста.", + "custom-host": "Хост користувача", + "custom-host-required": "Необхідний спеціальний хост.", + "port": "Порт", + "port-required": "Необхідно вказати порт.", + "port-range": "Порт має бути в діапазоні від 1 до 65535.", + "connect-timeout": "Час очікування з'єднання (сек)", + "connect-timeout-required": " Необхідно вказати час з'єднання підключення.", + "connect-timeout-range": "Час очікування з'єднання має бути в діапазоні від 1 до 200.", + "client-id": "ID клієнта", + "clean-session": "Очистити сеанс", + "enable-ssl": "Увімкнути SSL", + "credentials": "Авторизаційні дані", + "credentials-type": "Тип авторизаційних даних", + "credentials-type-required": "Необхідно вказати тип авторизаційних даних.", + "username": "Ім'я користувача", + "username-required": "Необхідно вказати ім'я користувача.", + "password": "Пароль", + "password-required": "Необхідно вказати пароль.", + "ca-cert": "Файл сертифіката CA *", + "private-key": "Файл приватного ключа *", + "private-key-password": "Пароль приватного ключа", + "cert": "Файл сертифіката*", + "no-file": "Не вибрано жодного файлу.", + "drop-file": "Перетягніть файл, або клацніть, щоб вибрати файл для завантаження.", + "topic-filters": "Тематичні фільтри", + "remove-topic-filter": "Видалити фільтр тем", + "add-topic-filter": "Додати фільтр тем", + "add-topic-filter-prompt": "Будь ласка, додайте фільтр тем", + "topic": "Тема", + "mqtt-qos": "QoS", + "mqtt-qos-at-most-once": "Не більше одного разу", + "mqtt-qos-at-least-once": "Принаймні, один раз", + "mqtt-qos-exactly-once": "Точно один раз", + "downlink-topic-pattern": "Downlink topic pattern", + "downlink-topic-pattern-required": "Downlink topic pattern is required.", + "aws-iot-endpoint": "AWS IoT Endpoint", + "aws-iot-endpoint-required": "AWS IoT Endpoint is required.", + "aws-iot-credentials": "Авторизаційні дані AWS IoT", + "application-credentials": "Авторизаційні дані додатків", + "api-key": "API ключ", + "api-key-required": "Необхідно вказати API ключ.", + "auth-token": "Маркер аутентифікації", + "auth-token-required": "Необхідно вказати маркер аутентифікації.", + "region": "Регіон", + "region-required": "Необхідно вказати регіон.", + "application-id": "ID програми", + "application-id-required": "Необхідно вказати ID програми.", + "access-key": "Ключ доступу", + "access-key-required": "Необхідно вказати ключ доступу.", + "connection-parameters": "Параметри підключення", + "service-bus-namespace-name": "Service Bus Namespace Name", + "service-bus-namespace-name-required": "Необхідно вказати Service Bus Namespace Name is required.", + "event-hub-name": "Event Hub Name", + "event-hub-name-required": "Необхідно вказати Event Hub Name is required.", + "sas-key-name": "Назва ключа SAS", + "sas-key-name-required": "Необхідно вказати назву ключа SAS.", + "sas-key": "Ключ SAS", + "sas-key-required": "SAS Key is required.", + "iot-hub-name": "IoT Hub Name (required for downlink)", + "metadata": "Метадані", + "type": "Тип", + "type-required": "Необхідно вказати тип.", + "uplink-converter": "Конвертер передачі даних", + "uplink-converter-required": "Необхідно вказати конвертер передачі даних.", + "downlink-converter": "Downlink data converter", + "type-http": "HTTP", + "type-ocean-connect": "OceanConnect", + "type-sigfox": "SigFox", + "type-thingpark": "ThingPark", + "type-tmobile-iot-cdp": "T-Mobile – IoT CDP", + "type-mqtt": "MQTT", + "type-aws-iot": "AWS IoT", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ttn": "TheThingsNetwork", + "type-azure-event-hub": "Azure Event Hub", + "type-opc-ua": "OPC-UA", + "type-ffb": "FFB", + "opc-ua-application-name": "Назва програми", + "opc-ua-application-uri": "Application uri", + "opc-ua-scan-period-in-seconds": "Період сканування в секундах", + "opc-ua-scan-period-in-seconds-required": "Необхідно вказати період сканування в секундах", + "opc-ua-timeout": "Час очікування в мілісекундах", + "opc-ua-timeout-required": "Необхідно вказати час очікування в мілісекундах", + "opc-ua-security": "Безпека", + "opc-ua-security-required": "Необхідно задати безпеку", + "opc-ua-identity": "Ідентифікація", + "opc-ua-identity-required": "Необхідно вказати ідентифікацію", + "opc-ua-keystore": "Сховище ключів", + "add-opc-ua-keystore-prompt": "Будь ласка, додайте файл сховища ключів", + "opc-ua-keystore-required": "Необхідно вказати сховище ключів", + "opc-ua-type": "Тип", + "opc-ua-keystore-type": "Тип сховища ключів", + "opc-ua-keystore-type-required": "Необхідно вказати тип", + "opc-ua-keystore-location": "Розташування *", + "opc-ua-keystore-password": "Пароль", + "opc-ua-keystore-password-required": "Необхідно вказати пароль", + "opc-ua-keystore-alias": "Псевдонім", + "opc-ua-keystore-alias-required": "Необхідно вказати псевдонім", + "opc-ua-keystore-key-password": "Пароль ключа", + "opc-ua-keystore-key-password-required": "Необхідно вказати пароль ключа", + "opc-ua-mapping": "Зіставлення", + "add-opc-ua-mapping-prompt": "Будь ласка, додайте зіставлення", + "opc-ua-mapping-type": "Тип зіставлення", + "opc-ua-mapping-type-required": "Необхідно вказати тип зіставлення", + "opc-ua-device-node-pattern": "Шаблон вузла пристрою", + "opc-ua-device-node-pattern-required": "Необхідно вказати шаблон вузла пристрою", + "opc-ua-add-map": "Додати елемент зіставлення", + "subscription-tags": "Теги передплати Теги підписки", + "remove-subscription-tag": "Видалити тег підписки", + "add-subscription-tag": "Додати тег підписки", + "add-subscription-tag-prompt": "Будь ласка, додайте тег підписки", + "key": "Ключ", + "path": "Шлях", + "required": "Необхідно" + }, + "item": { + "selected": "Вибрані" + }, + "js-func": { + "no-return-error": "Функція повинна повертати значення!", + "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!", + "tidy": "Tidy" + }, + "key-val": { + "key": "Ключ", + "value": "Значення", + "remove-entry": "Видалити елемент", + "add-entry": "Додати елемент", + "no-data": "Елементи відсутні" + }, + "layout": { + "layout": "Макет", + "manage": "Керування макетами", + "settings": "Налаштування макета", + "color": "Колір", + "main": "Основний", + "right": "Правий", + "select": "Вибрати макет" + }, + "legend": { + "direction": "Розташування елементів легенди", + "position": "Розташування легенди", + "show-max": "Показати максимальне значення", + "show-min": "Показати мінімальне значення ", + "show-avg": "Показати середнє значення", + "show-total": "Показати суму", + "settings": "Налаштування легенди", + "min": "мін", + "max": "макс", + "avg": "середнє", + "total": "Сума", + "comparison-time-ago": { + "days": "(день тому)", + "weeks": "(тиждень тому)", + "months": "(місяць тому)", + "years": "(рік тому)" + } + }, + "login": { + "login": "Вхід", + "request-password-reset": "Запит скидання пароля", + "reset-password": "Скинути пароль", + "create-password": "Створити пароль", + "passwords-mismatch-error": "Введені паролі повинні бути однаковими!", + "password-again": "Введіть пароль ще раз", + "sign-in": "Будь ласка, увійдіть в систему", + "username": "Ім'я користувача (ел. пошта)", + "remember-me": "Запам'ятати мене", + "forgot-password": "Забули пароль?", + "password-reset": "Скидання пароля", + "expired-password-reset-message": "Термін дії Вашого паролю закінчився! Будь ласка, створіть новий пароль.", + "new-password": "Новий пароль", + "new-password-again": "Повторіть новий пароль", + "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!", + "email": "Електронна пошта" + }, + "position": { + "top": "Угорі", + "bottom": "Знизу", + "left": "Ліворуч", + "right": "Праворуч" + }, + "profile": { + "profile": "Профіль", + "last-login-time": "Час останнього входу", + "change-password": "Змінити пароль", + "current-password": "Поточний пароль" + }, + "relation": { + "relations": "Відношення", + "direction": "Напрямок", + "search-direction": { + "FROM": "З", + "TO": "До" + }, + "direction-type": { + "FROM": "з", + "TO": "до" + }, + "from-relations": "Вихідні відношення", + "to-relations": "Вхідні відношення", + "selected-relations": "Вибрано { count, plural, 1 {1 відношення} other {# відношення} }", + "type": "Тип", + "to-entity-type": "До типу сутності", + "to-entity-name": "До імені сутності", + "from-entity-type": "Від типу сутності", + "from-entity-name": "Від імені сутності", + "to-entity": "До сутності", + "from-entity": "Від сутності", + "delete": "Видалити відношення", + "relation-type": "Тип відношення", + "relation-type-required": "Необхідно вказати тип відношення.", + "any-relation-type": "Будь-який тип", + "add": "Додати відношення", + "edit": "Редагувати відношення", + "delete-to-relation-title": "Ви впевнені, що хочете видалити відношення до сутності '{{entityName}}'?", + "delete-to-relation-text": "Будьте обережні, після підтвердження, сутність '{{entityName}}' не буде пов'язана з поточним об'єктом.", + "delete-to-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", + "delete-to-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення стануть не пов'язані з поточною сутністю.", + "delete-from-relation-title": "Ви впевнені, що хочете видалити зв'язок, який йде від сутності '{{entityName}}'?", + "delete-from-relation-text": "Будьте обережні, після підтвердження поточна сутність буде відв'язана від сутності '{{entityName}}'.", + "delete-from-relations-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 відношення} other {# відношення} }?", + "delete-from-relations-text": "Будьте обережні, після підтвердження, всі вибрані відношення будуть видалені, а поточна сутність стане не зв'язаною з відповідними сутностями.", + "remove-relation-filter": "Видалити фільтр відношення", + "add-relation-filter": "Додати фільтр відношення", + "any-relation": "Будь-яке відношення", + "relation-filters": "Фільтри відношення", + "additional-info": "Додаткова інформація (JSON)", + "invalid-additional-info": "Не вдалося розібрати JSON з додатковою інформацією ." + }, + "rulechain": { + "rulechain": "Ланцюг правил", + "rulechains": "Ланцюги правил", + "root": "Основний", + "delete": "Видалити ланцюг правил", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "description": "Опис", + "add": "Додати ланцюг правил", + "set-root": "Зробити ланцюг правил основним", + "set-root-rulechain-title": "Ви впевнені, що хочете зробити ланцюг правил '{{ruleChainName}}' основним?", + "set-root-rulechain-text": "Після підтвердження ланцюг правил стане основним і буде обробляти всі вхідні транспортні повідомлення.", + "delete-rulechain-title": "Ви впевнені, що хочете видалити ланцюг правил '{{ruleChainName}}'?", + "delete-rulechain-text": "Будьте обережні, після підтвердження ланцюг правил і всі пов'язані з ним дані стануть недоступними.", + "delete-rulechains-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }?", + "delete-rulechains-action-title": "Видалити{ count, plural, 1 {1 ланцюг правил} other {# ланцюги правил} }", + "delete-rulechains-text": "Будьте обережні, після підтвердження, вибрані ланцюги правил і всі пов'язані з ними дані стануть недоступними.", + "add-rulechain-text": "Додати новий ланцюг правил", + "no-rulechains-text": "Ланцюг правил не знайдено", + "rulechain-details": "Деталі ланцюга правил", + "details": "Деталі", + "events": "Події", + "system": "Система", + "import": "Імпортувати ланцюг правил", + "export": "Експортувати ланцюг правил", + "export-failed-error": "Не вдалося експортувати ланцюг правил: {{error}}", + "create-new-rulechain": "Створити новий ланцюг правил", + "rulechain-file": "Файл ланцюга правил", + "invalid-rulechain-file-error": "Неможливо імпортувати ланцюг правил: недійсна структуру даних ланцюга правил.", + "copyId": "Копіювати Id ланцюга правил", + "idCopiedMessage": "Id ланцюга правил скопійовано в буфер обміну", + "select-rulechain": "Вибрати ланцюг правил", + "no-rulechains-matching": "Не знайдено жодних ланцюгів правил, які відповідають '{{entity}}'.", + "rulechain-required": "Необхідно вказати ланцюг правил", + "management": "Управління ланцюгами правил", + "debug-mode": "Режим налагодження" + }, + "rulenode": { + "details": "Деталі", + "events": "Події", + "search": "Пошук вузлів", + "open-node-library": "Відкрити бібліотеку вузлів", + "add": "Додати вузол правил", + "name": "Ім'я", + "name-required": "Необхідно вказати ім'я.", + "type": "Тип", + "description": "Опис", + "delete": "Видалити вузол правил", + "select-all-objects": "Вибрати усі вузли та з'єднання", + "deselect-all-objects": "Зняти виділення з усіх вузлів і з'єднань", + "delete-selected-objects": "Видалити вибрані вузли та з'єднання", + "delete-selected": "Видалити вибране", + "select-all": "Вибрати все", + "copy-selected": "Копіювати вибране", + "deselect-all": "Відмінити вибране", + "rulenode-details": "Деталі вузла правил", + "debug-mode": "Режим налагодження", + "configuration": "Конфігурація", + "link": "Посилання", + "link-details": "Деталі посилання про вузол правил", + "add-link": "Додати посилання", + "link-label": "Мітка посилання", + "link-label-required": "Необхідно вказати мітку посилання.", + "custom-link-label": "Мітка посилання користувача", + "custom-link-label-required": "Необхідно вказати мітку посилання користувача.", + "link-labels": "Мітки посилання", + "link-labels-required": "Необхідно вказати мітки посилання.", + "no-link-labels-found": "Не знайдено жодних міток посилання", + "no-link-label-matching": "Мітка'{{label}}' не знайдена.", + "create-new-link-label": "Створити нову!", + "type-filter": "Фільтр", + "type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами", + "type-enrichment": "Насичення", + "type-enrichment-details": "Додати додаткову інформацію до метаданих повідомлень", + "type-transformation": "Трансформація", + "type-transformation-details": "Змінити склад повідомлення та його метадані", + "type-action": "Дія", + "type-action-details": "Виконати задану дію", + "type-analytics": "Аналітика", + "type-analytics-details": "Виконує аналіз потокових або збережених даних", + "type-external": "Зовнішній", + "type-external-details": "Взаємодіє з зовнішньою системою", + "type-rule-chain": "Ланцюг правил", + "type-rule-chain-details": "Перенаправити вхідне повідомлення на вказаний ланцюг правил", + "type-input": "Вхід", + "type-input-details": "Логічний вхід ланцюга правил, перенаправляє вхідні повідомлення на наступний пов'язаний вузол правил", + "type-unknown": "Невідомий", + "type-unknown-details": "Невизначений вузол правил", + "directive-is-not-loaded": "Вказана директива конфігурації '{{directiveName}}' недоступна.", + "ui-resources-load-error": "Не вдалося завантажити UI ресурси.", + "invalid-target-rulechain": "Не вдається визначити цільовий ланцюг правил!", + "test-script-function": "Протестувати скрипт", + "message": "Повідомлення", + "message-type": "Тип повідомлення", + "select-message-type": "Вибрати тип повідомлення", + "message-type-required": "Необхідно вказати тип повідомлення", + "metadata": "Метадані", + "metadata-required": "Записи метаданих не можуть бути порожніми.", + "output": "Вихід", + "test": "Тест", + "help": "Допомога", + "reset-debug-mode": "Вимкнути режим налогодження у всіх правилах" + }, + "scheduler": { + "scheduler": "Планувальник", + "scheduler-event": "Подія планувальника", + "select-scheduler-event": "Вибрати подію", + "no-scheduler-events-matching": "Не знайдено жодних подій, які відповідають '{{entity}}'.", + "scheduler-event-required": "Необхвдно вказати заплановану подію", + "management": "Управління планувальником", + "scheduler-events": "Планування подій", + "add-scheduler-event": "Додати подію", + "search-scheduler-events": "Пошук події", + "created-time": "Час створення", + "name": "Ім'я", + "type": "Тип", + "created_customer": "Створено клієнтом", + "edit-scheduler-event": "Редагувати подію", + "delete-scheduler-event": "Видалити подію", + "no-scheduler-events": "Не знайдено жодних запланованих подій", + "selected-scheduler-events": "{ count, plural, 1 {1 запланована подія} other {# заплановані події} } вибрано", + "delete-scheduler-event-title": "Ви впевнені, що хочете видалити подію '{{schedulerEventName}}'?", + "delete-scheduler-event-text": "Будьте обережні, після підтвердження подія і всі пов'язані з нею дані стануть недоступними.", + "delete-scheduler-events-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 запланована подія} other {# заплановані події} }?", + "delete-scheduler-events-text": "Будьте обережні, після підтвердження всі вибрані події будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "create": "Створити подію планувальника", + "edit": "Змінити подію планувальника", + "name-required": "Необхідно задати ім'я", + "configuration": "Конфігурація", + "schedule": "Розклад", + "start": "Початок", + "date": "Дата", + "time": "Час", + "repeat": "Повтор", + "repeats": "Повтори", + "daily": "Щодня", + "weekly": "Щотижня", + "repeats-required": "Потрібно вказати повторення.", + "repeat-on": "Повторювати по", + "repeat-every": "Повторювати кожний(у)", + "ends-on": "Завершення", + "sunday-label": "Нд", + "monday-label": "Пн", + "tuesday-label": "Вт", + "wednesday-label": "Ср", + "thursday-label": "Чт", + "friday-label": "Пт", + "saturday-label": "Сб", + "repeat-on-sunday": "Повторити у неділю", + "repeat-on-monday": "Повторити в понеділок", + "repeat-on-tuesday": "Повторити у вівторок", + "repeat-on-wednesday": "Повторити в середу", + "repeat-on-thursday": "Повторити в червер", + "repeat-on-friday": "Повторити в п'ятницю", + "repeat-on-saturday": "Повторити в суботу", + "event-type": "Тип події", + "select-event-type": "Вибрати тип події", + "event-type-required": "Необхідно вказати типи події.", + "list-mode": "Перегляд списку", + "calendar-mode": "Перегляд календаря", + "calendar-view-type": "Тип перегляду календаря", + "month": "Місяць", + "week": "Тиждень", + "day": "День", + "agenda-week": "Порядок тижня", + "agenda-day": "Порялок дня", + "list-year": "Список року", + "list-month": "Список місяця", + "list-week": "Список тижня", + "list-day": "Список дня", + "today": "Сьогодні", + "navigate-before": "Перейти до", + "navigate-next": "Перейти далі", + "starting-from": "Починаючи з", + "until": "до", + "on": "в", + "sunday": "Неділя", + "monday": "Понеділок", + "tuesday": "Вівторок", + "wednesday": "Середа", + "thursday": "Четвер", + "friday": "П'ятниця", + "saturday": "Субота", + "originator": "Ініціатор", + "single-entity": "Єдина сутність", + "group-of-entities": "Група сутностей", + "single-device": "Один пристрій", + "group-of-devices": "Група пристроїв", + "message-body": "Текст повідомлення", + "target": "Ціль", + "rpc-method": "Метод", + "rpc-method-required": "Необхідно вказати метод", + "rpc-params": "Параметри", + "select-dashboard-state": "Виберіть стан панелі візуалізації", + "hours": "Години", + "minutes": "Хвилини", + "seconds": "Секунди", + "time-interval-required": "Необхідно вказати часовий інтервал", + "time-unit-required": "Необхідно вказати одиниці часу" + }, + "report": { + "report-config": "Конфігурація звіту", + "email-config": "Конфігурація електронної пошти", + "dashboard-state-param": "Значення параметра стану панелі візуалізації", + "base-url": "Базова URL-адреса", + "base-url-required": "Необхідно вказати базову URL-адресу.", + "use-dashboard-timewindow": "Використовуйте вікно часу на панелі інструментів", + "timewindow": "Вікно часу", + "name-pattern": "Шаблон імені звіту", + "name-pattern-required": "Необхідно задати шаблон назви звіту", + "type": "Report type", + "use-current-user-credentials": "Використовувати поточні авторизаційні дані користувача", + "customer-user-credentials": "Авторизаційні дані користувачів", + "customer-user-credentials-required": "Необхідно задати авторизаційні дані користувачів", + "generate-test-report": "Створити звіт про перевірку", + "send-email": "Відправити лист", + "from": "Від", + "from-required": "Необхідно вказати від кого.", + "to": "До", + "to-required": "Необхідно вказати до кого.", + "cc": "Cc", + "bcc": "Bcc", + "subject": "Тема", + "subject-required": "Необхідно вказати тему.", + "body": "Текст", + "body-required": "Лист не може бути пустим." + }, + "blob-entity": { + "blob-entity": "Blob сутності", + "select-blob-entity": "Вибрати blob сутності", + "no-blob-entities-matching": "Не знайдено жодних сутностей blob, які відповідають '{{entity}}'.", + "blob-entity-required": "Необхідно вказати blob сутності", + "files": "Файли", + "search": "Пошук файлів", + "clear-search": "Очистити пошук", + "no-blob-entities-prompt": "Файлів не знайдено", + "report": "Звіт", + "created-time": "Час створення", + "name": "Ім'я", + "type": "Тип", + "created_customer": "Створено клієнтом", + "download-blob-entity": "Завантажити файл", + "delete-blob-entity": "Видалити файл", + "delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?", + "delete-blob-entity-text": "Будьте обережні, після підствердження, дані файлу стануть недоступними." + }, + "timezone": { + "timezone": "Часовий пояс", + "select-timezone": "Вибрати часовий пояс ", + "no-timezones-matching": "Не знайдено жодних часових поясів, які відповідають '{{timezone}}'.", + "timezone-required": "Необхідно вказати часовий пояс." + }, + "tenant": { + "tenant": "Власник", + "tenants": "Власники", + "management": "Управління власниками", + "add": "Додати власника", + "admins": "Адміністратори", + "manage-tenant-admins": "Керування адміністраторами власника", + "delete": "Видалити власника", + "add-tenant-text": "Додати нового власника", + "no-tenants-text": "Не знайдено жодного власника", + "tenant-details": "Подробиці про власника", + "delete-tenant-title": "Ви впевнені, що хочете видалити власника'{{tenantTitle}}'?", + "delete-tenant-text": "Будьте обережні, після підтвердження власник і всі пов'язані з ним дані стануть недоступними.", + "delete-tenants-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 власник} other {# власники} }?", + "delete-tenants-action-title": "Видалити { count, plural, 1 {1 власник} other {# власники} }", + "delete-tenants-text": "Будьте обережні, після підтвердження, усі вибрані власники будуть видалені, і всі пов'язані з ними дані стануть недоступними.", + "title": "Назва", + "title-required": "Необхідно вказати назву.", + "description": "Опис", + "details": "Деталі", + "events": "Події", + "copyId": "Копіювати Id власника", + "idCopiedMessage": "Id власника скопійовано в буфер обміну", + "select-tenant": "Вибрати власника", + "no-tenants-matching": "Не знайдено жодних власників, які відповідають '{{entity}}'.", + "tenant-required": "Необхідно вказати власника", + "selected-tenants": "{ count, plural, 1 {1 власник} other {# власники} } вибрано", + "search": "Пошук власників" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 секунда} other {# секунди} }", + "minutes-interval": "{ minutes, plural, 1 {1 хвилина} other {# хвилини} }", + "hours-interval": "{ hours, plural, 1 {1 година} other {# години} }", + "days-interval": "{ days, plural, 1 {1 день} other {# дні} }", + "days": "Дні", + "hours": "Години", + "minutes": "Хвилини", + "seconds": "Секунди", + "advanced": "Додатково" + }, + "timewindow": { + "days": "{ days, plural, 1 { день } other {# дні } }", + "hours": "{ hours, plural, 0 { годин } 1 {1 година } other {# години } }", + "minutes": "{ minutes, plural, 0 { хвилин } 1 {1 хвилина } other {# хвилини } }", + "seconds": "{ seconds, plural, 0 { секунд } 1 {1 секунда } other {# секунди } }", + "realtime": "Реальний час", + "history": "Історія", + "last-prefix": "Останнє", + "period": "з {{ startTime }} до {{ endTime }}", + "edit": "Редагувати вікно часу", + "date-range": "Проміжок часу", + "last": "Останнє", + "time-period": "Період часу", + "hide": "Приховати" + }, + "user": { + "user": "Користувач", + "users": "Користувачі", + "customer-users": "Користувачі клієнта", + "tenant-admins": "Адміністратори власників", + "sys-admin": "Системний адміністратор", + "tenant-admin": "Адміністратор власника", + "customer": "Клієнт", + "anonymous": "Анонім", + "add": "Додати користувача", + "delete": "Видалити користувача", + "add-user-text": "Додати нового користувача", + "no-users-text": "Не знайдено жодного користувача", + "user-details": "Подробиці про користувача", + "delete-user-title": "Ви впевнені, що хочете видалити користувача'{{userEmail}}'?", + "delete-user-text": "Будьте обережні, після підтвердження, користувач і всі пов'язані з ним дані стануть недоступними.", + "delete-users-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 користувача} other {# користувачів} }?", + "delete-users-action-title": "Видалити { count, plural, 1 {1 користувача} other {# користувачів} }", + "delete-users-text": "Будьте обережні, після підтвердження, усіх виділених користувачів буде видалено, і всі пов'язані з ними дані стануть недоступними.", + "activation-email-sent-message": "Повідомлення про активацію успішно надіслано!", + "resend-activation": "Повторно надіслати активацію", + "email": "Електронна пошта", + "email-required": "Необхідно вказати електронну пошту.", + "invalid-email-format": "Недійсний формат електронної пошти.", + "first-name": "Ім'я", + "last-name": "Прізвище", + "description": "Опис", + "default-dashboard": "Стандартна панель візуалізації", + "always-fullscreen": "Завжди в повноекранному режимі", + "select-user": "Вибрати користувача", + "no-users-matching": "Не знайдено жодного користувача, що відповідає '{{entity}}'.", + "user-required": "Необхідно вказати користувача", + "activation-method": "Спосіб активації", + "display-activation-link": "Показати посилання для активації", + "send-activation-mail": "Надіслати активаційного листа", + "activation-link": "Активаційне посилання для користувача", + "activation-link-text": "Для активувації користувача, скористайтеся наступним activation link :", + "copy-activation-link": "Скопіювати активаційне посилання ", + "activation-link-copied-message": "Посилання на активацію користувача було скопійовано в буфер обміну", + "selected-users": "{ count, plural, 1 {1 користувач} other {# користувачі} } вибрано", + "search": "Пошук користувачів", + "details": "Подробиці", + "login-as-tenant-admin": "Увійти як адміністратор власника", + "login-as-customer-user": "Увійти як користувач клієнта", + "disable-account": "Вимкнути обліковий запис користувача", + "enable-account": "Увімкнути обліковий запис користувача", + "enable-account-message": "Обліковий запис користувача успішно увімкнено!", + "disabled-account-message": "Обліковий запис користувача успішно вимкнено!" + }, + "value": { + "type": "Тип значення", + "string": "Рядок", + "string-value": "Значення рядка", + "integer": "Ціле", + "integer-value": "Ціле значення", + "invalid-integer-value": "Недійсне ціле значення", + "double": "Подвійне", + "double-value": "Подвійне значення", + "boolean": "Логічне", + "boolean-value": "Логічне значення", + "false": "Помилкове", + "true": "Правдиве", + "long": "Довге" + }, + "widget": { + "widget-library": "Бібліотека віджетів", + "widget-bundle": "Пакет віджетів", + "select-widgets-bundle": "Виберіть пакет віджетів", + "management": "Керування віджетами", + "editor": "Редактор віджетів", + "widget-type-not-found": "Помилка завантаження конфігурації віджетів.
Можливо, пов'язаний з нею\n тип віджета було видалено.", + "widget-type-load-error": "Віджет не вдалося завантажити з наступних причин:", + "remove": "Видалити віджет", + "edit": "Відредагувати віджет", + "remove-widget-title": "Ви впевнені, що хочете видалити віджет '{{widgetTitle}}'?", + "remove-widget-text": "Після підтвердження віджет і всі пов'язані з ним дані стануть недоступними.", + "timeseries": "Телеметрія", + "search-data": "Пошук даних", + "no-data-found": "Даних не знайдено", + "latest-values": "Останні значення", + "rpc": "Керуючий віджет", + "alarm": "Віджет сигнала тривоги", + "static": "Статичний віджет", + "select-widget-type": "Вибрати тип віджета", + "missing-widget-title-error": "Необхідно вказати назву віджета!", + "widget-saved": "Віджет збережено", + "unable-to-save-widget-error": "Неможливо зберегти віджет! Віджет має помилки!", + "save": "Зберегти віджет", + "saveAs": "Зберегти віджет як", + "save-widget-type-as": "Зберегти тип віджета як", + "save-widget-type-as-text": "Введіть новий заголовок віджета та / або виберіть цільові віджети", + "toggle-fullscreen": "Перейти в повноекранний режим", + "run": "Запустити віджет", + "title": "Назва віджета", + "title-required": "Необхідно вказати назву віджета.", + "type": "Тип віджета", + "resources": "Ресурси", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Видалити ресурс", + "add-resource": "Додати ресурс", + "html": "HTML", + "tidy": "Форматувати", + "css": "CSS", + "settings-schema": "Схема налаштувань", + "datakey-settings-schema": "Схема налаштувань ключів даних", + "javascript": "Javascript", + "remove-widget-type-title": "Ви впевнені, що хочете видалити тип віджета '{{widgetName}}'?", + "remove-widget-type-text": "Будьте обережні, після підтвердження, тип віджета і всі пов'язані з ним дані стануть недоступними.", + "remove-widget-type": "Видалити тип віджета", + "add-widget-type": "Додати новий тип віджета", + "widget-type-load-failed-error": "Не вдалося завантажити тип віджета!", + "widget-template-load-failed-error": "Не вдалося завантажити шаблон віджета!", + "add": "Додати віджет", + "undo": "Скасувати зміни віджета", + "export": "Експртувати віджет", + "export-data": "Експортувати дані віджетів", + "export-to-csv": "Експортувати дані в CSV...", + "export-to-excel": "Експортувати дані в XLS..." + }, + "widget-action": { + "header-button": "Кнопка заголовка віджета", + "open-dashboard-state": "Перейти до нового стану панелі візуалізації", + "update-dashboard-state": "Оновити поточний стан панелі візуалізації", + "open-dashboard": "Перейти до іншої панелі візуалізації", + "custom": "Дії користувачів", + "custom-pretty": "Дії користувачів (з HTML шаблоном)", + "target-dashboard-state": "Цільовий стан панелі візуалізації", + "target-dashboard-state-required": "Необхідно вказати цільовий стан панелі візуалізації", + "set-entity-from-widget": "Встановити сутність із віджета", + "target-dashboard": "Цільова панель візуалізації", + "open-right-layout": "Відкрити мобільний режим панелі візуалізації" + }, + "widgets-bundle": { + "current": "Поточний зв'язок", + "widgets-bundles": "Пакети віджетів", + "add": "Додати пакет віджетів", + "delete": "Видалити пакет віджетів", + "title": "Назва", + "title-required": "Необхідно вказати назву віджета.", + "add-widgets-bundle-text": "Додати новий пакет віджетів", + "no-widgets-bundles-text": "Не знайдено жодних пакетів віджетів", + "empty": "Пакет віджетів порожній", + "details": "Подробиці", + "widgets-bundle-details": "Деталі пакетів віджетів", + "delete-widgets-bundle-title": "Ви впевнені, що хочете видалити пакет віджетів '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Будьте обережні, після підтвердження, пакети віджетів і всі пов'язані з ними дані стануть недоступними.", + "delete-widgets-bundles-title": "Ви впевнені, що хочете видалити { count, plural, 1 {пакет віджетів} other {# пакети віджетів} }?", + "delete-widgets-bundles-action-title": "Видалити { count, plural, 1 {1 пакет віджетів} other {# пакет віджетів} }", + "delete-widgets-bundles-text": "Будьте обережні, після підтвердження, всі виділені пакети віджетів і всі пов'язані з ними дані стануть недоступними.", + "no-widgets-bundles-matching": "Не знайдено жодних пакетів віджетів, які відповідають '{{widgetsBundle}}'.", + "widgets-bundle-required": "Необхідно вказати пакет віджетів.", + "system": "Системний", + "import": "Імпортувати пакет віджетів", + "export": "Експортувати пакет віджетів", + "export-failed-error": "Неможливо експортувати пакет віджетів: {{error}}", + "create-new-widgets-bundle": "Створити новий пакет віджетів", + "widgets-bundle-file": "Файл набору віджетів", + "invalid-widgets-bundle-file-error": "Неможливо імпортувати пакет віджетів: недійсна структура даних пакету віджетів." + }, + "widget-config": { + "data": "Дані", + "settings": "Налаштування", + "advanced": "Додатково", + "title": "Назва", + "general-settings": "Загальні налаштування", + "display-title": "Відобразити назву", + "drop-shadow": "Тінь", + "enable-fullscreen": "Увімкнути повноекранний режим", + "enable-data-export": "Увімкнути експорт даних", + "background-color": "Колір фону", + "text-color": "Колір тексту", + "padding": "Відступ", + "margin": "Границі", + "widget-style": "Стиль віджетів", + "title-style": "Стиль заголовка", + "mobile-mode-settings": "Налаштування мобільного режиму", + "order": "Порядок", + "height": "Висота", + "units": "Спеціальний символ після значення", + "decimals": "Кількість цифр після коми", + "timewindow": "Вікно часу", + "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", + "display-timewindow": "Показувати вікно часу", + "display-legend": "Показати легенду", + "datasources": "Джерела даних", + "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", + "datasource-type": "Тип", + "datasource-parameters": "Параметри", + "remove-datasource": "Видалити джерело даних", + "add-datasource": "Додати джерело даних", + "target-device": "Цільовий пристрій", + "alarm-source": "Джерело сигнала тривоги", + "actions": "Дії", + "action": "Дія", + "add-action": "Додати дію", + "search-actions": "Пошук дії", + "action-source": "Джерело дії", + "action-source-required": "Необхідно вказати джерело дії.", + "action-name": "Ім'я дії", + "action-name-required": "Необхідно вказати ім'я дії.", + "action-name-not-unique": "Дія з такою назвою вже існує.
Назва дії має бути унікальною в межах одного джерела дії.", + "action-icon": "Іконка", + "action-type": "Тип", + "action-type-required": "Необхідно вказати тип дії.", + "edit-action": "Редагувати дію", + "delete-action": "Видалити дію", + "delete-action-title": "Видалити дію віджета", + "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?", + "display-icon": "Показувати іконку у назві", + "icon-color": "Колір іконки", + "icon-size": "Розмір іконки" + }, + "widget-type": { + "import": "Імпортувати тип віджета", + "export": "Експортувати тип віджета", + "export-failed-error": "Неможливо експортувати тип віджета: {{error}}", + "create-new-widget-type": "Створити новий тип віджета", + "widget-type-file": "Файл типу віджета", + "invalid-widget-type-file-error": "Неможливо імпортувати тип віджету: неправильна структура даних типу віджета." + }, + "widgets": { + "date-range-navigator": { + "localizationMap": { + "Sun": "Нд", + "Mon": "Пн", + "Tue": "Вт", + "Wed": "Ср", + "Thu": "Чт", + "Fri": "Пт", + "Sat": "Сб", + "Jan": "Січ.", + "Feb": "Лют.", + "Mar": "Берез.", + "Apr": "Квіт.", + "May": "Трав.", + "Jun": "Черв.", + "Jul": "Лип.", + "Aug": "Серп.", + "Sep": "Верес.", + "Oct": "Жовт.", + "Nov": "Листоп.", + "Dec": "Груд.", + "January": "Січень", + "February": "Лютий", + "March": "Березень", + "April": "Квітень", + "June": "Червень", + "July": "Липень", + "August": "Серпень", + "September": "Вересень", + "October": "Жовтень", + "November": "Листопад", + "December": "Грудень", + "Custom Date Range": "Користувацький діапазон дат", + "Date Range Template": "Шаблон діапазону дат", + "Today": "Сьогодні", + "Yesterday": "Вчора", + "This Week": "Цього тижня", + "Last Week": "Минулий тиждень", + "This Month": "Цей місяць", + "Last Month": "Минулий місяць", + "Year": "Рік", + "This Year": "Цього року", + "Last Year": "Минулий рік", + "Date picker": "Вибір дати", + "Hour": "Година", + "Day": "День", + "Week": "Тиждень", + "2 weeks": "2 Тижні", + "Month": "Місяць", + "3 months": "3 Місяці", + "6 months": "6 Місяців", + "Custom interval": "Користувацький інтервал", + "Interval": "Інтервал", + "Step size": "Розмір кроку", + "Ok": "Ok" + } + }, + "input-widgets": { + "attribute-not-allowed": "Атрибут не може бути вибраний в цьому віджеті", + "date": "Дата", + "blocked-location": "Геолокація заблокована у вашому браузері", + "claim-device": "Підтвердити пристрій", + "claim-failed": "Не вдалося підтвердити пристрій!", + "claim-not-found": "Пристрій не знайдено!", + "claim-successful": "Пристрій успішно підтверджено!", + "discard-changes": "Скасувати зміни", + "device-name": "Назва пристрою", + "device-name-required": "Необхідно вказати назву пристрою", + "entity-attribute-required": "Значення атрибута обов'язкове", + "entity-coordinate-required": "Необхідно вказати широту та довготу", + "entity-timeseries-required": "Значення телеметрії обов'язкове", + "get-location": "Отримати поточне місцезнаходження", + "latitude": "Широта", + "longitude": "Довгота", + "not-allowed-entity": "Обрана сутність не має спільних атрибутів", + "no-attribute-selected": "Атрибут не вибрано", + "no-datakey-selected": "Ні один datakey не обраний", + "no-entity-selected": "Сутність не вибрано", + "no-coordinate-specified": "Ключ для широти/довготи не вказаний", + "no-support-geolocation": "Ваш браузер не підтримує геолокацію", + "no-image": "Немає зображення", + "no-support-web-camera": "Нет поддерживаемой веб-камеры", + "no-timeseries-selected": "Параметр телеметрії не вибрано", + "secret-key": "Секретний ключ", + "secret-key-required": "Необхідно вказати секретний ключ", + "switch-attribute-value": "Змінити значення атрибута", + "switch-camera": "Змінити камеру", + "switch-timeseries-value": "Змінити значення телеметрії", + "take-photo": "Зробити фото", + "time": "Час", + "timeseries-not-allowed": "Телеметрія не може бути вибрана в цьому віджеті", + "update-failed": "Не вдалося оновити", + "update-successful": "Успішно оновлено", + "update-attribute": "Оновити атрибут", + "update-timeseries": "Оновити телеметрію", + "value": "Значення" + } + }, + "white-labeling": { + "white-labeling": "Білий маркування", + "login-white-labeling": "Login White Labeling", + "preview": "Попередній перегляд", + "app-title": "Назва програми", + "favicon": "Іконка веб-сайту", + "favicon-description": "*.ico, *.gif or *.png image with maximum size {{kbSize}} KBytes.", + "favicon-size-error": "Зображення веб-сайту завелике. Максимально дозволений розмір зображення веб-сайту {{kbSize}} KBytes.", + "favicon-type-error": "Недійсний формат файлу зображення веб-сайту. Приймаються лише зображення ICO, GIF або PNG.", + "drop-favicon-image": "Зніміть зображення піктограми веб-сайту або клацніть, щоб вибрати файл для завантаження.", + "no-favicon-image": "Не вибрано жодної іконки", + "logo": "Логотип", + "logo-description": "Будь-яке зображення з максимальним розміром {{kbSize}} KBytes.", + "logo-size-error": "Зображення логотипу занадто велике. Максимально дозволений розмір зображення логотипу{{kbSize}} KBytes.", + "logo-type-error": "Недійсний формат файлу логотипу. Приймаються тільки зображення.", + "drop-logo-image": "Зніміть зображення логотипу або клацніть, щоб вибрати файл для завантаження.", + "no-logo-image": "Не вибрано жожного логотипу", + "logo-height": "Висота логотипу, px", + "primary-palette": "Основна палітра", + "accent-palette": "Палітра акцент", + "customize-palette": "Налаштування", + "edit-palette": "Редагувати палітру", + "save-palette": "Зберегти палітру", + "primary-background": "Первинний фон", + "secondary-background": "Вторинний фон", + "hue1": "HUE 1", + "hue2": "HUE 2", + "hue3": "HUE 3", + "page-background-color": "Колір фону сторінки", + "dark-foreground": "Темний передній план", + "domain-name": "Доменне ім'я" + }, + "icon": { + "icon": "Іконка", + "select-icon": "Виберіть Іконку", + "material-icons": "Іконки в стилі Material", + "show-all": "Показати всі іконки" + }, + "custom": { + "widget-action": { + "action-cell-button": "Кнопка дії в комірці таблиці", + "row-click": "Клацніть на рядок", + "marker-click": "Клацніть на маркер", + "polygon-click": "Дія при натисканні на полігон", + "tooltip-tag-action": "Дія при натисканні на посилання в підказці", + "node-selected": "Дії при виборі ноди", + "element-click": "Дії при натисканні на HTML елементі", + "pie-slice-click": "Дії при натисканні на секції кругової діаграми", + "row-double-click": "Дії при подвійному натисканні на рядок" + } + }, + "language": { + "language": "Мова", + "locales": { + "fr_FR": "Французька", + "zh_CN": "Китайська", + "en_US": "Англійська", + "it_IT": "Італійська", + "ko_KR": "Корейська", + "ru_RU": "Російська", + "es_ES": "Іспанська", + "ja_JA": "Японська", + "tr_TR": "Турецька", + "de_DE": "Німецька", + "uk_UA": "Українська", + "fa_IR": "Перська", + "cs_CZ": "Чеська", + "el_GR": "Грецька" + } + } +} \ No newline at end of file diff --git a/ui/src/app/locale/locale.constant-zh_CN.json b/ui/src/app/locale/locale.constant-zh_CN.json index a683828a8a..a7f1cb9e62 100644 --- a/ui/src/app/locale/locale.constant-zh_CN.json +++ b/ui/src/app/locale/locale.constant-zh_CN.json @@ -241,7 +241,8 @@ "select-asset": "选择资产", "no-assets-matching": "没有找到匹配 '{{entity}}' 的资产。", "asset-required": "资产必填", - "name-starts-with": "资产名称以此开头" + "name-starts-with": "资产名称以此开头", + "label": "标签" }, "attribute": { "attributes": "属性", diff --git a/ui/src/app/widget/lib/add-entity-panel.scss b/ui/src/app/widget/lib/add-entity-panel.scss new file mode 100644 index 0000000000..6d93e2a4da --- /dev/null +++ b/ui/src/app/widget/lib/add-entity-panel.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-add-entity-panel { + min-width: 150px; + max-height: 200px; + overflow: hidden; + overflow-y: auto; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); +} diff --git a/ui/src/app/widget/lib/add-entity-panel.tpl.html b/ui/src/app/widget/lib/add-entity-panel.tpl.html new file mode 100644 index 0000000000..738a921679 --- /dev/null +++ b/ui/src/app/widget/lib/add-entity-panel.tpl.html @@ -0,0 +1,22 @@ + + + + {{ entity.entityLabel || entity.name }} + + diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js index 9fb41851ad..174e72eab3 100644 --- a/ui/src/app/widget/lib/alarms-table-widget.js +++ b/ui/src/app/widget/lib/alarms-table-widget.js @@ -94,6 +94,24 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia icon: 'search' }; + let columnDisplayAction = { + name: 'entity.columns-to-display', + show: true, + onAction: function($event) { + vm.editColumnsToDisplay($event); + }, + icon: 'view_column' + }; + + let statusFilterAction = { + name: 'alarm.alarm-status-filter', + show: true, + onAction: function($event) { + vm.editAlarmStatusFilter($event); + }, + icon: 'filter_list' + }; + vm.enterFilterMode = enterFilterMode; vm.exitFilterMode = exitFilterMode; vm.onReorder = onReorder; @@ -167,7 +185,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia function initializeConfig() { - vm.ctx.widgetActions = [ vm.searchAction ]; + vm.ctx.widgetActions = [ vm.searchAction, statusFilterAction, columnDisplayAction ]; vm.displayDetails = angular.isDefined(vm.settings.displayDetails) ? vm.settings.displayDetails : true; vm.allowAcknowledgment = angular.isDefined(vm.settings.allowAcknowledgment) ? vm.settings.allowAcknowledgment : true; @@ -215,6 +233,8 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia vm.enableSelection = angular.isDefined(vm.settings.enableSelection) ? vm.settings.enableSelection : true; vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; + columnDisplayAction.show = angular.isDefined(vm.settings.enableSelectColumnDisplay) ? vm.settings.enableSelectColumnDisplay : true; + statusFilterAction.show = angular.isDefined(vm.settings.enableStatusFilter) ? vm.settings.enableStatusFilter : true; if (!vm.allowAcknowledgment && !vm.allowClear) { vm.enableSelection = false; } diff --git a/ui/src/app/widget/lib/alarms-table-widget.tpl.html b/ui/src/app/widget/lib/alarms-table-widget.tpl.html index 802ea5de99..03e2289912 100644 --- a/ui/src/app/widget/lib/alarms-table-widget.tpl.html +++ b/ui/src/app/widget/lib/alarms-table-widget.tpl.html @@ -63,28 +63,7 @@ {{ key.title }} - - - filter_list - - - {{'alarm.alarm-status-filter' | translate}} - - - - view_column - - - {{'entity.columns-to-display' | translate}} - - - + diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js index c7ff63b00f..4da445ea0f 100644 --- a/ui/src/app/widget/lib/entities-table-widget.js +++ b/ui/src/app/widget/lib/entities-table-widget.js @@ -91,6 +91,15 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $mdP icon: 'search' }; + let columnDisplayAction = { + name: 'entity.columns-to-display', + show: true, + onAction: function($event) { + vm.editColumnsToDisplay($event); + }, + icon: 'view_column' + }; + vm.enterFilterMode = enterFilterMode; vm.exitFilterMode = exitFilterMode; vm.onReorder = onReorder; @@ -147,7 +156,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $mdP function initializeConfig() { - vm.ctx.widgetActions = [ vm.searchAction ]; + vm.ctx.widgetActions = [ vm.searchAction, columnDisplayAction ]; vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton'); @@ -162,6 +171,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $mdP vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true; vm.displayEntityLabel = angular.isDefined(vm.settings.displayEntityLabel) ? vm.settings.displayEntityLabel : false; + columnDisplayAction.show = angular.isDefined(vm.settings.enableSelectColumnDisplay) ? vm.settings.enableSelectColumnDisplay : true; if (vm.settings.entityNameColumnTitle && vm.settings.entityNameColumnTitle.length) { vm.entityNameColumnTitle = utils.customTranslation(vm.settings.entityNameColumnTitle, vm.settings.entityNameColumnTitle); diff --git a/ui/src/app/widget/lib/entities-table-widget.tpl.html b/ui/src/app/widget/lib/entities-table-widget.tpl.html index 5f14623bcb..0ddfb9bd43 100644 --- a/ui/src/app/widget/lib/entities-table-widget.tpl.html +++ b/ui/src/app/widget/lib/entities-table-widget.tpl.html @@ -42,18 +42,7 @@ {{ column.title }} - - - view_column - - - {{'entity.columns-to-display' | translate}} - - - + diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 6a26e6bec2..08496d574b 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -23,6 +23,7 @@ import 'flot/src/plugins/jquery.flot.selection'; import 'flot/src/plugins/jquery.flot.pie'; import 'flot/src/plugins/jquery.flot.crosshair'; import 'flot/src/plugins/jquery.flot.stack'; +import 'flot/src/plugins/jquery.flot.symbol'; import 'flot.curvedlines/curvedLines'; /* eslint-disable angular/angularelement */ @@ -32,6 +33,7 @@ export default class TbFlot { this.ctx = ctx; this.chartType = chartType || 'line'; var settings = ctx.settings; + var utils = this.ctx.$scope.$injector.get('utils'); ctx.tooltip = $('#flot-series-tooltip'); if (ctx.tooltip.length === 0) { @@ -59,7 +61,7 @@ export default class TbFlot { divElement.css({ display: "flex", alignItems: "center", - justifyContent: "flex-start" + justifyContent: "space-between" }); var lineSpan = $(''); lineSpan.css({ @@ -125,55 +127,99 @@ export default class TbFlot { } else { ctx.tooltipFormatter = function(hoverInfo, seriesIndex) { var content = ''; - var timestamp = parseInt(hoverInfo.time); - var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); - var dateDiv = $('
' + date + '
'); - dateDiv.css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "4px", - fontWeight: "700" - }); - content += dateDiv.prop('outerHTML'); + if (tbFlot.ctx.tooltipIndividual) { - var found = hoverInfo.seriesHover.filter((seriesHover) => { + var seriesHoverArray; + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { + seriesHoverArray = hoverInfo[0].seriesHover.concat(hoverInfo[1].seriesHover); + } else { + seriesHoverArray = hoverInfo[0].seriesHover; + } + var found = seriesHoverArray.filter((seriesHover) => { return seriesHover.index === seriesIndex; }); if (found && found.length) { + let timestamp; + if (!angular.isNumber(hoverInfo[0].time) || (found[0].time < hoverInfo[0].time)) { + timestamp = parseInt(hoverInfo[1].time); + } else { + timestamp = parseInt(hoverInfo[0].time); + } + let date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); + let dateDiv = $('
' + date + '
'); + dateDiv.css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "4px", + fontWeight: "700" + }); + content += dateDiv.prop('outerHTML'); content += seriesInfoDivFromInfo(found[0], seriesIndex); } } else { - var seriesDiv = $('
'); - seriesDiv.css({ - display: "flex", - flexDirection: "row" - }); - const maxRows = 15; - var columns = Math.ceil(hoverInfo.seriesHover.length / maxRows); - var columnsContent = ''; - for (var c = 0; c < columns; c++) { - var columnDiv = $('
'); - columnDiv.css({ - display: "flex", - flexDirection: "column" - }); - var columnContent = ''; - for (var i = c*maxRows; i < (c+1)*maxRows; i++) { - if (i == hoverInfo.seriesHover.length) { - break; + var maxRows; + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { + maxRows = 5; + } else { + maxRows = 15; + } + var columns = 0; + if (hoverInfo[1] && (hoverInfo[1].seriesHover.length > hoverInfo[0].seriesHover.length)) { + columns = Math.ceil(hoverInfo[1].seriesHover.length / maxRows); + } else { + columns = Math.ceil(hoverInfo[0].seriesHover.length / maxRows); + } + + for (var j = 0; j < hoverInfo.length; j++) { + var hoverData = hoverInfo[j]; + if (angular.isNumber(hoverData.time)) { + var columnsContent = ''; + let timestamp = parseInt(hoverData.time); + let date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); + let dateDiv = $('
' + date + '
'); + dateDiv.css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "4px", + fontWeight: "700" + }); + content += dateDiv.prop('outerHTML'); + + var seriesDiv = $('
'); + seriesDiv.css({ + display: "flex", + flexDirection: "row" + }); + for (var c = 0; c < columns; c++) { + var columnDiv = $('
'); + columnDiv.css({ + display: "flex", + flexDirection: "column", + flex: "1" + }); + var columnContent = ''; + for (var i = c*maxRows; i < (c+1)*maxRows; i++) { + if (i >= hoverData.seriesHover.length) { + break; + } + var seriesHoverInfo = hoverData.seriesHover[i]; + columnContent += seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex); + } + columnDiv.html(columnContent); + + if (columnContent) { + if (c > 0) { + columnsContent += ''; + } + columnsContent += columnDiv.prop('outerHTML'); + } } - var seriesHoverInfo = hoverInfo.seriesHover[i]; - columnContent += seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex); - } - columnDiv.html(columnContent); - if (c > 0) { - columnsContent += ''; + seriesDiv.html(columnsContent); + content += seriesDiv.prop('outerHTML'); } - columnsContent += columnDiv.prop('outerHTML'); } - seriesDiv.html(columnsContent); - content += seriesDiv.prop('outerHTML'); } return content; }; @@ -185,6 +231,7 @@ export default class TbFlot { ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false); ctx.tooltipCumulative = angular.isDefined(settings.tooltipCumulative) ? settings.tooltipCumulative : false; + ctx.hideZeros = angular.isDefined(settings.hideZeros) ? settings.hideZeros : false; var font = { color: settings.fontColor || "#545454", @@ -209,7 +256,8 @@ export default class TbFlot { }; if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { - options.xaxis = { + options.xaxes = []; + this.xaxis = { mode: 'time', timezone: 'browser', font: angular.copy(font), @@ -220,16 +268,11 @@ export default class TbFlot { labelFont: angular.copy(font) }; if (settings.xaxis) { - if (settings.xaxis.showLabels === false) { - options.xaxis.tickFormatter = function() { - return ''; - }; - } - options.xaxis.font.color = settings.xaxis.color || options.xaxis.font.color; - options.xaxis.label = settings.xaxis.title || null; - options.xaxis.labelFont.color = options.xaxis.font.color; - options.xaxis.labelFont.size = options.xaxis.font.size+2; - options.xaxis.labelFont.weight = "bold"; + this.xaxis.font.color = settings.xaxis.color || this.xaxis.font.color; + this.xaxis.label = utils.customTranslation(settings.xaxis.title, settings.xaxis.title) || null; + this.xaxis.labelFont.color = this.xaxis.font.color; + this.xaxis.labelFont.size = this.xaxis.font.size+2; + this.xaxis.labelFont.weight = "bold"; } ctx.yAxisTickFormatter = function(value/*, axis*/) { @@ -263,7 +306,7 @@ export default class TbFlot { this.yaxis.font.color = settings.yaxis.color || this.yaxis.font.color; this.yaxis.min = angular.isDefined(settings.yaxis.min) ? settings.yaxis.min : null; this.yaxis.max = angular.isDefined(settings.yaxis.max) ? settings.yaxis.max : null; - this.yaxis.label = settings.yaxis.title || null; + this.yaxis.label = utils.customTranslation(settings.yaxis.title, settings.yaxis.title) || null; this.yaxis.labelFont.color = this.yaxis.font.color; this.yaxis.labelFont.size = this.yaxis.font.size+2; this.yaxis.labelFont.weight = "bold"; @@ -296,7 +339,7 @@ export default class TbFlot { options.grid.borderWidth = angular.isDefined(settings.grid.outlineWidth) ? settings.grid.outlineWidth : 1; if (settings.grid.verticalLines === false) { - options.xaxis.tickLength = 0; + this.xaxis.tickLength = 0; } if (settings.grid.horizontalLines === false) { this.yaxis.tickLength = 0; @@ -309,14 +352,42 @@ export default class TbFlot { } } - options.crosshair = { - mode: 'x' + options.xaxes[0] = angular.copy(this.xaxis); + + if (settings.xaxis && settings.xaxis.showLabels === false) { + options.xaxes[0].tickFormatter = function() { + return ''; + }; } - options.series = { - stack: settings.stack === true + if (settings.comparisonEnabled) { + var xaxis = angular.copy(this.xaxis); + xaxis.position = 'top'; + if (settings.xaxisSecond) { + if (settings.xaxisSecond.showLabels === false) { + xaxis.tickFormatter = function() { + return ''; + }; + } + xaxis.label = utils.customTranslation(settings.xaxisSecond.title, settings.xaxisSecond.title) || null; + xaxis.position = settings.xaxisSecond.axisPosition; + } + xaxis.tickLength = 0; + options.xaxes.push(xaxis); + + options.series = { + stack: false + }; + } else { + options.series = { + stack: settings.stack === true + }; } + options.crosshair = { + mode: 'x' + }; + if (this.chartType === 'line' && settings.smoothLines) { options.series.curvedLines = { active: true, @@ -429,6 +500,13 @@ export default class TbFlot { series.lines = { fill: keySettings.fillLines === true }; + + if (this.ctx.settings.stack && !this.ctx.settings.comparisonEnabled) { + series.stack = !keySettings.excludeFromStacking; + } else { + series.stack = false; + } + if (this.chartType === 'line' || this.chartType === 'state') { series.lines.show = keySettings.showLines !== false } else { @@ -445,14 +523,23 @@ export default class TbFlot { }; if (keySettings.showPoints === true) { series.points.show = true; - series.points.lineWidth = 5; + series.points.lineWidth = angular.isDefined(keySettings.showPointsLineWidth) ? keySettings.showPointsLineWidth : 5; series.points.radius = angular.isDefined(keySettings.showPointsRadius) ? keySettings.showPointsRadius : 3; + series.points.symbol = angular.isDefined(keySettings.showPointShape) ? keySettings.showPointShape : 'circle'; + if (series.points.symbol == 'custom' && keySettings.pointShapeFormatter) { + try { + series.points.symbol = new Function('ctx, x, y, radius, shadow', keySettings.pointShapeFormatter); + } catch (e) { + series.points.symbol = 'circle'; + } + } + } if (this.chartType === 'line' && this.ctx.settings.smoothLines && !series.points.show) { series.curvedLines = { apply: true - } + }; } var lineColor = tinycolor(series.dataKey.color); @@ -460,6 +547,15 @@ export default class TbFlot { series.highlightColor = lineColor.toRgbString(); + if (series.datasource.isAdditional) { + series.xaxisIndex = 1; + series.xaxis = 2; + } else { + series.xaxisIndex = 0; + series.xaxis = 1; + } + + if (this.yaxis) { var units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.ctx.trackUnits; var yaxis; @@ -477,7 +573,7 @@ export default class TbFlot { series.yaxisIndex = this.yaxes.indexOf(yaxis); series.yaxis = series.yaxisIndex+1; yaxis.keysInfo[i] = {hidden: false}; - yaxis.hidden = false; + yaxis.show = true; } } @@ -491,8 +587,12 @@ export default class TbFlot { this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; } } - this.options.xaxis.min = this.subscription.timeWindow.minTime; - this.options.xaxis.max = this.subscription.timeWindow.maxTime; + this.options.xaxes[0].min = this.subscription.timeWindow.minTime; + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.ctx.settings.comparisonEnabled) { + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; + } } this.checkMouseEvents(); @@ -500,6 +600,7 @@ export default class TbFlot { if (this.ctx.plot) { this.ctx.plot.destroy(); } + if (this.chartType === 'pie' && this.ctx.animatedPie) { this.ctx.pieDataAnimationDuration = 250; this.pieData = angular.copy(this.subscription.data); @@ -608,8 +709,12 @@ export default class TbFlot { } } - this.options.xaxis.min = this.subscription.timeWindow.minTime; - this.options.xaxis.max = this.subscription.timeWindow.maxTime; + this.options.xaxes[0].min = this.subscription.timeWindow.minTime; + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.ctx.settings.comparisonEnabled) { + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; + } if (this.chartType === 'bar') { if (this.subscription.timeWindowConfig.aggregation && this.subscription.timeWindowConfig.aggregation.type === 'NONE') { this.options.series.bars.barWidth = this.ctx.defaultBarWidth; @@ -623,6 +728,10 @@ export default class TbFlot { } else { this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime; this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.ctx.settings.comparisonEnabled) { + this.ctx.plot.getOptions().xaxes[1].min = this.subscription.comparisonTimeWindow.minTime; + this.ctx.plot.getOptions().xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime; + } if (this.chartType === 'bar') { if (this.subscription.timeWindowConfig.aggregation && this.subscription.timeWindowConfig.aggregation.type === 'NONE') { this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.defaultBarWidth; @@ -902,6 +1011,11 @@ export default class TbFlot { "type": "string", "default": "" }; + properties["hideZeros"] = { + "title": "Hide zero/false values from tooltip", + "type": "boolean", + "default": false + }; properties["grid"] = { "title": "Grid settings", @@ -1029,6 +1143,7 @@ export default class TbFlot { "key": "tooltipValueFormatter", "type": "javascript" }); + schema["form"].push("hideZeros"); schema["form"].push({ "key": "grid", "items": [ @@ -1079,6 +1194,21 @@ export default class TbFlot { } ] }); + if (chartType === 'graph' || chartType === 'bar') { + schema.groupInfoes = [{ + "formIndex":0, + "GroupTitle":"Common Settings" + }]; + schema.form = [schema.form]; + angular.merge(schema.schema.properties, chartSettingsSchemaForComparison.schema.properties); + schema.schema.required = schema.schema.required.concat(chartSettingsSchemaForComparison.schema.required); + schema.form.push(chartSettingsSchemaForComparison.form); + schema.groupInfoes.push({ + "formIndex":schema.groupInfoes.length, + "GroupTitle":"Comparison Settings" + }); + } + return schema; } @@ -1086,12 +1216,18 @@ export default class TbFlot { return {} } - static datakeySettingsSchema(defaultShowLines) { - return { + static datakeySettingsSchema(defaultShowLines, chartType) { + + var schema = { "schema": { "type": "object", "title": "DataKeySettings", "properties": { + "excludeFromStacking": { + "title": "Exclude from stacking(available in \"Stacking\" mode)", + "type": "boolean", + "default": false + }, "showLines": { "title": "Show lines", "type": "boolean", @@ -1107,6 +1243,25 @@ export default class TbFlot { "type": "boolean", "default": false }, + "showPointShape": { + "title": "Select point shape:", + "type": "string", + "default": "circle" + }, + "pointShapeFormatter": { + "title": "Point shape format function, f(ctx, x, y, radius, shadow)", + "type": "string", + "default": "var size = radius * Math.sqrt(Math.PI) / 2;\n" + + "ctx.moveTo(x - size, y - size);\n" + + "ctx.lineTo(x + size, y + size);\n" + + "ctx.moveTo(x - size, y + size);\n" + + "ctx.lineTo(x + size, y - size);" + }, + "showPointsLineWidth": { + "title": "Line width of points", + "type": "number", + "default": 5 + }, "showPointsRadius": { "title": "Radius of points", "type": "number", @@ -1161,9 +1316,46 @@ export default class TbFlot { "required": ["showLines", "fillLines", "showPoints"] }, "form": [ + "excludeFromStacking", "showLines", "fillLines", "showPoints", + { + "key": "showPointShape", + "type": "rc-select", + "multiple": false, + "items": [ + { + "value": "circle", + "label": "Circle" + }, + { + "value": "cross", + "label": "Cross" + }, + { + "value": "diamond", + "label": "Diamond" + }, + { + "value": "square", + "label": "Square" + }, + { + "value": "triangle", + "label": "Triangle" + }, + { + "value": "custom", + "label": "Custom function" + } + ] + }, + { + "key": "pointShapeFormatter", + "type": "javascript" + }, + "showPointsLineWidth", "showPointsRadius", { "key": "tooltipValueFormatter", @@ -1195,7 +1387,47 @@ export default class TbFlot { "type": "javascript" } ] + }; + + var properties = schema["schema"]["properties"]; + if (chartType === 'graph' || chartType === 'bar') { + properties["comparisonSettings"] = { + "title": "Comparison Settings", + "type": "object", + "properties": { + "showValuesForComparison": { + "title": "Show historical values for comparison", + "type": "boolean", + "default": true + }, + "comparisonValuesLabel": { + "title": "Historical values label", + "type": "string", + "default": "" + }, + "color": { + "title": "Color", + "type": "string", + "default": "" + } + }, + "required": ["showValuesForComparison"] + }; + schema["form"].push({ + "key": "comparisonSettings", + "items": [ + "comparisonSettings.showValuesForComparison", + "comparisonSettings.comparisonValuesLabel", + { + "key": "comparisonSettings.color", + "type": "color" + } + ] + }); + } + + return schema; } enableMouseEvents() { @@ -1227,10 +1459,15 @@ export default class TbFlot { tooltipHtml = tbFlot.ctx.tooltipFormatter(item); } else { var hoverInfo = tbFlot.getHoverInfo(tbFlot.ctx.plot.getData(), pos); - if (angular.isNumber(hoverInfo.time)) { - hoverInfo.seriesHover.sort(function (a, b) { + if (angular.isNumber(hoverInfo[0].time) || (hoverInfo[1] && angular.isNumber(hoverInfo[1].time))) { + hoverInfo[0].seriesHover.sort(function (a, b) { return b.value - a.value; }); + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) { + hoverInfo[1].seriesHover.sort(function (a, b) { + return b.value - a.value; + }); + } tooltipHtml = tbFlot.ctx.tooltipFormatter(hoverInfo, item ? item.seriesIndex : -1); } } @@ -1258,9 +1495,11 @@ export default class TbFlot { }); if (multipleModeTooltip) { - for (var i = 0; i < hoverInfo.seriesHover.length; i++) { - var seriesHoverInfo = hoverInfo.seriesHover[i]; - tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex); + for (var j = 0; j < hoverInfo.length; j++) { + for (var i = 0; i < hoverInfo[j].seriesHover.length; i++) { + var seriesHoverInfo = hoverInfo[j].seriesHover[i]; + tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex); + } } } } @@ -1399,19 +1638,38 @@ export default class TbFlot { getHoverInfo (seriesList, pos) { - var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime; + var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime, hoverData; var last_value = 0; - var results = { + var results = [{ seriesHover: [] - }; + }]; + if (this.ctx.settings.comparisonEnabled) { + results.push({ + seriesHover: [] + }); + var minDistanceHistorical, minTimeHistorical; + } for (i = 0; i < seriesList.length; i++) { series = seriesList[i]; - hoverIndex = this.findHoverIndexFromData(pos.x, series); + var posx; + if (series.datasource.isAdditional) { + posx = pos.x2; + } else { + posx = pos.x; + } + hoverIndex = this.findHoverIndexFromData(posx, series); if (series.data[hoverIndex] && series.data[hoverIndex][0]) { - hoverDistance = pos.x - series.data[hoverIndex][0]; + hoverDistance = posx - series.data[hoverIndex][0]; pointTime = series.data[hoverIndex][0]; - if (!minDistance + if (series.datasource.isAdditional) { + if (!minDistanceHistorical + || (hoverDistance >= 0 && (hoverDistance < minDistanceHistorical || minDistanceHistorical < 0)) + || (hoverDistance < 0 && hoverDistance > minDistanceHistorical)) { + minDistanceHistorical = hoverDistance; + minTimeHistorical = pointTime; + } + } else if (!minDistance || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) || (hoverDistance < 0 && hoverDistance > minDistance)) { minDistance = hoverDistance; @@ -1429,23 +1687,33 @@ export default class TbFlot { } if (series.stack || (series.curvedLines && series.curvedLines.apply)) { - hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); + hoverIndex = this.findHoverIndexFromDataPoints(posx, series, hoverIndex); + } + if (!this.ctx.hideZeros || value) { + hoverData = { + value: value, + hoverIndex: hoverIndex, + color: series.dataKey.color, + label: series.dataKey.label, + units: series.dataKey.units, + decimals: series.dataKey.decimals, + tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction, + time: pointTime, + distance: hoverDistance, + index: i + }; + if (series.datasource.isAdditional) { + results[1].seriesHover.push(hoverData); + } else { + results[0].seriesHover.push(hoverData); + } } - results.seriesHover.push({ - value: value, - hoverIndex: hoverIndex, - color: series.dataKey.color, - label: series.dataKey.label, - units: series.dataKey.units, - decimals: series.dataKey.decimals, - tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction, - time: pointTime, - distance: hoverDistance, - index: i - }); } } - results.time = minTime; + if (results[1] && results[1].seriesHover.length) { + results[1].time = minTimeHistorical; + } + results[0].time = minTime; return results; } @@ -1524,4 +1792,93 @@ export default class TbFlot { } } +const chartSettingsSchemaForComparison = { + "schema": { + "title": "Comparison Settings", + "type": "object", + "properties": { + "comparisonEnabled": { + "title": "Enable comparison", + "type": "boolean", + "default": false + }, + "timeForComparison": { + "title": "Time to show historical data", + "type": "string", + "default": "months" + }, + "xaxisSecond": { + "title": "Second X axis", + "type": "object", + "properties": { + "axisPosition": { + "title": "Axis position", + "type": "string", + "default": "top" + }, + "showLabels": { + "title": "Show labels", + "type": "boolean", + "default": true + }, + "title": { + "title": "Axis title", + "type": "string", + "default": null + } + } + } + }, + "required": [] + }, + "form": [ + "comparisonEnabled", + { + "key": "timeForComparison", + "type": "rc-select", + "multiple": false, + "items": [ + { + "value": "days", + "label": "Day ago" + }, + { + "value": "weeks", + "label": "Week ago" + }, + { + "value": "months", + "label": "Month ago (default)" + }, + { + "value": "years", + "label": "Year ago" + } + ] + }, + { + "key": "xaxisSecond", + "items": [ + { + "key": "xaxisSecond.axisPosition", + "type": "rc-select", + "multiple": false, + "items": [ + { + "value": "top", + "label": "Top (default)" + }, + { + "value": "bottom", + "label": "Bottom" + } + ] + }, + "xaxisSecond.showLabels", + "xaxisSecond.title", + ] + } + ] +}; + /* eslint-enable angular/angularelement */ diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js index 649290ef1e..5a42f2672a 100644 --- a/ui/src/app/widget/lib/google-map.js +++ b/ui/src/app/widget/lib/google-map.js @@ -19,7 +19,7 @@ var gmGlobals = { } export default class TbGoogleMap { - constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, gmApiKey, gmDefaultMapType, defaultCenterPosition) { + constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, gmApiKey, gmDefaultMapType, defaultCenterPosition, markerClusteringSetting) { var tbMap = this; this.utils = utils; @@ -29,6 +29,7 @@ export default class TbGoogleMap { this.tooltips = []; this.defaultMapType = gmDefaultMapType; this.defaultCenterPosition = defaultCenterPosition; + this.isMarketCluster = markerClusteringSetting && markerClusteringSetting.isMarketCluster; function clearGlobalId() { if (gmGlobals.loadingGmId && gmGlobals.loadingGmId === tbMap.mapId) { @@ -49,8 +50,12 @@ export default class TbGoogleMap { zoom: tbMap.defaultZoomLevel || 8, center: new google.maps.LatLng(tbMap.defaultCenterPosition[0], tbMap.defaultCenterPosition[1]) // eslint-disable-line no-undef }); + if (tbMap.isMarketCluster){ + tbMap.markersCluster = new MarkerClusterer(tbMap.map, [], // eslint-disable-line no-undef + angular.merge({imagePath: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'}, markerClusteringSetting)); + } if (initCallback) { - initCallback(); + setTimeout(initCallback, 0);// eslint-disable-line } } @@ -87,7 +92,10 @@ export default class TbGoogleMap { this.initMapFunctionName = 'initGoogleMap_' + this.mapId; window[this.initMapFunctionName] = function() { // eslint-disable-line no-undef, angular/window-service - lazyLoad.load({ type: 'js', path: 'https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js' }).then( // eslint-disable-line no-undef + lazyLoad.load([ // eslint-disable-line no-undef + { type: 'js', path: 'https://unpkg.com/@google/markerwithlabel@1.2.3/src/markerwithlabel.js' }, + { type: 'js', path: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/markerclusterer.js' } + ]).then( function success() { gmGlobals.gmApiKeys[tbMap.apiKey].loaded = true; initGoogleMap(); @@ -105,6 +113,7 @@ export default class TbGoogleMap { ); }; + /* eslint-enable no-undef */ if (this.apiKey && this.apiKey.length > 0) { if (gmGlobals.gmApiKeys[this.apiKey]) { @@ -143,6 +152,10 @@ export default class TbGoogleMap { return angular.isDefined(this.map); } + getContainer() { + return this.isMarketCluster ? this.markersCluster : this.map; + } + /* eslint-disable no-undef */ updateMarkerLabel(marker, settings) { marker.set('labelContent', '
'+settings.labelText+'
'); @@ -221,17 +234,19 @@ export default class TbGoogleMap { /* eslint-enable no-undef */ /* eslint-disable no-undef */ - createMarker(location, dsIndex, settings, onClickListener, markerArgs) { + createMarker(location, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { var marker; if (settings.showLabel) { marker = new MarkerWithLabel({ position: location, labelContent: '
'+settings.labelText+'
', - labelClass: "tb-labels" + labelClass: "tb-labels", + draggable: settings.drraggable, }); } else { marker = new google.maps.Marker({ position: location, + draggable: settings.drraggable, }); } var gMap = this; @@ -240,7 +255,11 @@ export default class TbGoogleMap { if (settings.showLabel) { marker.set('labelAnchor', new google.maps.Point(100, iconInfo.size[1] + 20)); } - marker.setMap(gMap.map); + if(gMap.isMarketCluster) { + gMap.getContainer().addMarker(marker); + } else { + marker.setMap(gMap.getContainer()); + } }); if (settings.displayTooltip) { @@ -251,6 +270,10 @@ export default class TbGoogleMap { marker.addListener('click', onClickListener); } + if (onDragendListener) { + marker.addListener('dragend', onDragendListener); + } + return marker; } @@ -398,8 +421,8 @@ export default class TbGoogleMap { /* eslint-disable no-undef */ - fitBounds(bounds) { - if (this.dontFitMapBounds && this.defaultZoomLevel) { + fitBounds(bounds, useDefaultZoom) { + if ((this.dontFitMapBounds || useDefaultZoom) && this.defaultZoomLevel) { this.map.setZoom(this.defaultZoomLevel); this.map.setCenter(bounds.getCenter()); } else { @@ -459,4 +482,8 @@ export default class TbGoogleMap { return this.tooltips; } + getCenter() { + return this.map.getCenter().toJSON(); + } + } diff --git a/ui/src/app/widget/lib/image-map.js b/ui/src/app/widget/lib/image-map.js index 3b47be27ec..960de83691 100644 --- a/ui/src/app/widget/lib/image-map.js +++ b/ui/src/app/widget/lib/image-map.js @@ -300,12 +300,14 @@ export default class TbImageMap { onMarkerIconReady(iconInfo); } - createMarker(position, dsIndex, settings, onClickListener, markerArgs) { + createMarker(position, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { var pos = this.posFunction(position.x, position.y); var x = pos.x * this.width; var y = pos.y * this.height; var location = this.pointToLatLng(x, y); - var marker = L.marker(location, {});//.addTo(this.map); + var marker = L.marker(location, { + draggable: settings.drraggable + });//.addTo(this.map); marker.position = position; marker.offsetX = settings.markerOffsetX; marker.offsetY = settings.markerOffsetY; @@ -327,10 +329,30 @@ export default class TbImageMap { if (onClickListener) { marker.on('click', onClickListener); } + if (onDragendListener) { + marker.on('dragend', ($event) => { + let newMarkerPosition = this.latLngToPoint(marker.getLatLng()); + marker.position.x = this.constructor.calculateNewPosition(newMarkerPosition.x, this.width); + marker.position.y = this.constructor.calculateNewPosition(newMarkerPosition.y, this.height); + this.setMarkerPosition(marker, marker.position); + onDragendListener($event); + }); + } this.markers.push(marker); return marker; } + static calculateNewPosition(positon, imageSize) { + let newPosition = positon / imageSize; + if (newPosition < 0) { + newPosition = 0; + } else if (newPosition > 1) { + newPosition = 1; + } + return newPosition; + } + + updateMarkers() { this.markers.forEach((marker) => { this.updateMarkerLocation(marker); @@ -439,6 +461,10 @@ export default class TbImageMap { return this.tooltips; } + getCenter() { + return this.map.getCenter(); + } + } class Position { diff --git a/ui/src/app/widget/lib/map-widget2.js b/ui/src/app/widget/lib/map-widget2.js index 1c34a57716..c8d2428ded 100644 --- a/ui/src/app/widget/lib/map-widget2.js +++ b/ui/src/app/widget/lib/map-widget2.js @@ -21,10 +21,12 @@ import TbImageMap from './image-map'; import TbTencentMap from './tencent-map'; import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils'; +import addEntityPanelTemplate from './add-entity-panel.tpl.html'; +import './add-entity-panel.scss'; export default class TbMapWidgetV2 { - constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) { + constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element, isEdit) { var tbMap = this; this.ctx = ctx; this.mapProvider = mapProvider; @@ -33,6 +35,7 @@ export default class TbMapWidgetV2 { } this.utils = ctx.$scope.$injector.get('utils'); this.drawRoutes = drawRoutes; + this.isEdit = isEdit ? isEdit : false; this.markers = []; this.polygons = []; if (this.drawRoutes) { @@ -48,7 +51,7 @@ export default class TbMapWidgetV2 { }; if (settings.defaultZoomLevel) { - if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) { + if (settings.defaultZoomLevel >= 0 && settings.defaultZoomLevel < 21) { this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel); } } @@ -69,6 +72,46 @@ export default class TbMapWidgetV2 { var minZoomLevel = this.drawRoutes ? 18 : 15; + let markerClusteringSetting = { + isMarketCluster: false + }; + + if (settings.useClusterMarkers === true){ + if (mapProvider === 'google-map' || mapProvider === 'tencent-map') { + markerClusteringSetting = { + isMarketCluster: true, + zoomOnClick: settings.zoomOnClick, + averageCenter: true + }; + if(angular.isDefined(settings.maxZoom) && settings.maxZoom >= 0 && settings.maxZoom < 19){ + markerClusteringSetting.maxZoom = Math.floor(settings.maxZoom); + } + if(angular.isDefined(settings.gridSize) && settings.gridSize > 0){ + markerClusteringSetting.gridSize = Math.floor(settings.gridSize); + } + if(angular.isDefined(settings.minimumClusterSize) && settings.minimumClusterSize > 1){ + markerClusteringSetting.minimumClusterSize = Math.ceil(settings.minimumClusterSize); + } + } else if(mapProvider === 'openstreet-map' || mapProvider === 'here') { + markerClusteringSetting = { + isMarketCluster: true, + zoomToBoundsOnClick: settings.zoomOnClick, + showCoverageOnHover: settings.showCoverageOnHover, + removeOutsideVisibleBounds: settings.removeOutsideVisibleBounds, + animate: settings.animate, + chunkedLoading: settings.chunkedLoading + }; + if(angular.isDefined(settings.maxClusterRadius) && settings.maxClusterRadius > 0){ + markerClusteringSetting.maxClusterRadius = Math.floor(settings.maxClusterRadius); + } + if(angular.isDefined(settings.maxZoom) && settings.maxZoom >= 0 && settings.maxZoom < 19){ + markerClusteringSetting.disableClusteringAtZoom = Math.floor(settings.maxZoom); + } + } + } + + + var initCallback = function () { tbMap.update(); @@ -86,7 +129,7 @@ export default class TbMapWidgetV2 { let openStreetMapProvider = {}; if (mapProvider === 'google-map') { - this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType, settings.defaultCenterPosition); + this.map = new TbGoogleMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType, settings.defaultCenterPosition, markerClusteringSetting); } else if (mapProvider === 'openstreet-map') { if (settings.useCustomProvider && settings.customProviderTileUrl) { openStreetMapProvider.name = settings.customProviderTileUrl; @@ -94,10 +137,10 @@ export default class TbMapWidgetV2 { } else { openStreetMapProvider.name = settings.mapProvider; } - this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider, null,settings.defaultCenterPosition); + this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider, null,settings.defaultCenterPosition, markerClusteringSetting); } else if (mapProvider === 'here') { openStreetMapProvider.name = settings.mapProvider; - this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider, settings.credentials, settings.defaultCenterPosition); + this.map = new TbOpenStreetMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, openStreetMapProvider, settings.credentials, settings.defaultCenterPosition, markerClusteringSetting); } else if (mapProvider === 'image-map') { this.map = new TbImageMap(this.ctx, $element, this.utils, initCallback, settings.mapImageUrl, @@ -107,7 +150,7 @@ export default class TbMapWidgetV2 { settings.imageUrlAttribute, settings.useDefaultCenterPosition ? settings.defaultCenterPosition: null); } else if (mapProvider === 'tencent-map') { - this.map = new TbTencentMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType, settings.defaultCenterPosition); + this.map = new TbTencentMap($element, this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, settings.disableScrollZooming, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType, settings.defaultCenterPosition, markerClusteringSetting); } @@ -252,6 +295,112 @@ export default class TbMapWidgetV2 { } } + selectEntity($event) { + var tbMap = this; + + function setDefaultPosition(entity) { + let position = tbMap.map.getCenter(); + if (tbMap.mapProvider === "image-map") { + position = tbMap.map.latLngToPoint(position); + position.lat = position.x / tbMap.map.width; + position.lng = position.y / tbMap.map.height; + } + + tbMap.saveMarkerLocation( + entity, + locationsWithoutMarker[entitiesWithoutPosition.indexOf(entity)], + position + ); + } + + const element = angular.element($event.target); + const $mdPanel = this.ctx.$scope.$injector.get('$mdPanel'); + const $document = this.ctx.$scope.$injector.get('$document'); + let position = $mdPanel.newPanelPosition() + .relativeTo(element) + .addPanelPosition($mdPanel.xPosition.ALIGN_END, $mdPanel.yPosition.BELOW); + + let locationsWithoutMarker = this.locations.filter((location) => !location.marker); + let entitiesWithoutPosition = []; + for (let i = 0; i < locationsWithoutMarker.length; i++) { + entitiesWithoutPosition.push(this.subscription.datasources[locationsWithoutMarker[i].dsIndex]); + } + + if(entitiesWithoutPosition.length === 1){ + setDefaultPosition(entitiesWithoutPosition[0]); + } else { + let config = { + attachTo: angular.element($document[0].body), + controller: addEntityPanelController, + controllerAs: 'vm', + templateUrl: addEntityPanelTemplate, + panelClass: 'tb-add-entity-panel', + position: position, + fullscreen: false, + locals: { + 'entities': entitiesWithoutPosition, + 'onClose': setDefaultPosition + }, + openFrom: $event, + clickOutsideToClose: true, + escapeToClose: true, + focusOnOpen: false + }; + $mdPanel.open(config); + } + } + + saveMarkerLocation(datasource, location, coordinate) { + var tbMap = this; + + const types = tbMap.ctx.$scope.$injector.get('types'); + const $q = tbMap.ctx.$scope.$injector.get('$q'); + const attributeService = tbMap.ctx.$scope.$injector.get('attributeService'); + + let attributesLocation = []; + let timeseriesLocation = []; + let promises = []; + + let dataKeys = datasource.dataKeys; + for (let i = 0; i < dataKeys.length; i++) { + if (dataKeys[i].name === location.settings.latKeyName || dataKeys[i].name === location.settings.lngKeyName) { + let newLocation = { + key: dataKeys[i].name, + value: dataKeys[i].name === location.settings.latKeyName ? coordinate.lat : coordinate.lng + }; + if (dataKeys[i].type === types.dataKeyType.attribute) { + attributesLocation.push(newLocation); + } else if (dataKeys[i].type === types.dataKeyType.timeseries) { + timeseriesLocation.push(newLocation); + } + } + } + + if (attributesLocation.length > 0) { + promises.push(attributeService.saveEntityAttributes( + datasource.entityType, + datasource.entityId, + types.attributesScope.server.value, + attributesLocation, + { + ignoreLoading: true + } + )) + } + if (timeseriesLocation.length > 0) { + promises.push(attributeService.saveEntityTimeseries( + datasource.entityType, + datasource.entityId, + "scope", + timeseriesLocation, + { + ignoreLoading: true + } + )) + } + return $q.all([promises]); + } + update() { var tbMap = this; @@ -371,7 +520,10 @@ export default class TbMapWidgetV2 { function (event) { tbMap.callbacks.onLocationClick(location); locationRowClick(event, location); - }, [location.dsIndex]); + }, [location.dsIndex], + function (event) { + markerDragend(event, location) + }); tbMap.markers.push(location.marker); changed = true; } else { @@ -384,6 +536,22 @@ export default class TbMapWidgetV2 { return changed; } + function markerDragend($event, location) { + if (location.settings.drraggable) { + let position = tbMap.map.getMarkerPosition(location.marker); + if (tbMap.mapProvider === "image-map") { + position.lat = position.x; + position.lng = position.y; + delete position.x; + delete position.y; + } else if (tbMap.mapProvider === "google-map") { + position = position.toJSON(); + } + + tbMap.saveMarkerLocation(tbMap.subscription.datasources[location.dsIndex], location, position); + } + } + function locationRowClick($event, location) { var descriptors = tbMap.ctx.actionsApi.getActionDescriptors('markerClick'); if (descriptors.length) { @@ -527,6 +695,7 @@ export default class TbMapWidgetV2 { location.settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.tooltipPattern); location.settings.tooltipReplaceInfo = processPattern(location.settings.tooltipPattern, datasources, currentDatasourceIndex); } + location.settings.drraggable = tbMap.isEdit; tbMap.locations.push(location); updateLocation(location, data, dataMap); if (!tbMap.locationSettings.useDefaultCenterPosition) { @@ -563,7 +732,7 @@ export default class TbMapWidgetV2 { } }); - tbMap.map.fitBounds(bounds); + tbMap.map.fitBounds(bounds, tbMap.isEdit && tbMap.markers.length === 1); } function mapPolygonArray (rawArray) { @@ -612,7 +781,10 @@ export default class TbMapWidgetV2 { return !(!ds[tbMap.locationSettings.latKeyName] && !ds[tbMap.locationSettings.lngKeyName]); }); tbMap.initBounds = !dataValid; - tbMap.map.fitBounds(bounds); + + if(!tbMap.isEdit && tbMap.markers.length !== 1 || tbMap.polylines || tbMap.polygons) { + tbMap.map.fitBounds(bounds); + } } } @@ -676,7 +848,11 @@ export default class TbMapWidgetV2 { }) } } - map.fitBounds(bounds); + if((!map.isEdit && map.markers && map.markers.length !== 1) || + (this.polylines && this.polylines.length > 0) || + (this.polygons && this.polygons.length > 0)) { + map.fitBounds(bounds); + } } } } @@ -728,6 +904,24 @@ export default class TbMapWidgetV2 { "formIndex":schema.groupInfoes.length, "GroupTitle":"Route Map Settings" }); + } else if (mapProvider !== 'image-map'){ + angular.merge(schema.schema.properties, markerClusteringSettingsSchema.schema.properties); + schema.schema.required = schema.schema.required.concat(markerClusteringSettingsSchema.schema.required); + schema.form.push(markerClusteringSettingsSchema.form); + if (mapProvider === 'google-map' || mapProvider === 'tencent-map') { + angular.merge(schema.schema.properties, markerClusteringSettingsSchemaGoogle.schema.properties); + schema.schema.required = schema.schema.required.concat(markerClusteringSettingsSchemaGoogle.schema.required); + schema.form[schema.form.length -1] = schema.form[schema.form.length -1].concat(markerClusteringSettingsSchemaGoogle.form); + } + if (mapProvider === 'openstreet-map' || mapProvider === 'here') { + angular.merge(schema.schema.properties, markerClusteringSettingsSchemaLeaflet.schema.properties); + schema.schema.required = schema.schema.required.concat(markerClusteringSettingsSchemaLeaflet.schema.required); + schema.form[schema.form.length -1] = schema.form[schema.form.length -1].concat(markerClusteringSettingsSchemaLeaflet.form); + } + schema.groupInfoes.push({ + "formIndex":schema.groupInfoes.length, + "GroupTitle":"Markers Clustering Settings" + }); } return schema; } @@ -975,7 +1169,7 @@ const commonMapSettingsSchema = "type": "object", "properties": { "defaultZoomLevel": { - "title": "Default map zoom level (1 - 20)", + "title": "Default map zoom level (0 - 20)", "type": "number" }, "useDefaultCenterPosition": { @@ -1251,6 +1445,103 @@ const routeMapSettingsSchema = ] }; +const markerClusteringSettingsSchema = + { + "schema": { + "title": "Markers Clustering Configuration", + "type": "object", + "properties": { + "useClusterMarkers": { + "title": "Use map markers clustering", + "type": "boolean", + "default": false + }, + "zoomOnClick": { + "title": "Zoom when clicking on a cluster", + "type": "boolean", + "default": true + }, + "maxZoom": { + "title": "The maximum zoom level when a marker can be part of a cluster (0 - 18)", + "type": "number" + } + }, + "required": [] + }, + "form": [ + "useClusterMarkers", + "zoomOnClick", + "maxZoom" + ] + }; + +const markerClusteringSettingsSchemaGoogle = + { + "schema": { + "title": "Marker Clustering Configuration Google", + "type": "object", + "properties": { + "gridSize": { + "title": "Maximum radius that a cluster will cover in pixels", + "type": "number", + "default": 60 + }, + "minimumClusterSize": { + "title": "The minimum number of markers in a cluster", + "type": "number" + } + }, + "required": [] + }, + "form": [ + "gridSize", + "minimumClusterSize" + ] + }; + +const markerClusteringSettingsSchemaLeaflet = + { + "schema": { + "title": "Markers Clustering Configuration Leaflet", + "type": "object", + "properties": { + "showCoverageOnHover": { + "title": "Show the bounds of markers when mouse over a cluster", + "type": "boolean", + "default": true + }, + "animate": { + "title": "Show animation on markers when zooming", + "type": "boolean", + "default": true + }, + "maxClusterRadius": { + "title": "Maximum radius that a cluster will cover in pixels", + "type": "number", + "default": 80 + }, + "chunkedLoading": { + "title": "Use chunks for adding markers so that the page does not freeze", + "type": "boolean", + "default": false + }, + "removeOutsideVisibleBounds": { + "title": "Use lazy load for adding markers", + "type": "boolean", + "default": true + } + }, + "required": [] + }, + "form": [ + "showCoverageOnHover", + "animate", + "maxClusterRadius", + "chunkedLoading", + "removeOutsideVisibleBounds" + ] + }; + const imageMapSettingsSchema = { "schema": { @@ -1470,4 +1761,17 @@ const imageMapSettingsSchema = ] } ] - }; \ No newline at end of file + }; + +/*@ngInject*/ +function addEntityPanelController(mdPanelRef, entities) { + var vm = this; + vm.entities = entities; + vm.selectEntity = selectEntity; + + function selectEntity(entity) { + mdPanelRef.close().then(() => { + this.onClose(entity); + }); + } +} diff --git a/ui/src/app/widget/lib/multiple-input-widget.js b/ui/src/app/widget/lib/multiple-input-widget.js index ad8972ff7d..11b74fd697 100644 --- a/ui/src/app/widget/lib/multiple-input-widget.js +++ b/ui/src/app/widget/lib/multiple-input-widget.js @@ -41,27 +41,21 @@ function MultipleInputWidget() { } /*@ngInject*/ -function MultipleInputWidgetController($q, $scope, attributeService, toast, types, utils) { +function MultipleInputWidgetController($q, $scope, $translate, attributeService, toast, types, utils) { var vm = this; - vm.dataKeyDetected = false; - vm.hasAnyChange = false; vm.entityDetected = false; - vm.isValidParameter = true; - vm.message = 'No entity selected'; - - vm.rows = []; - vm.rowIndex = 0; + vm.isAllParametersValid = true; + vm.sources = []; vm.datasources = null; - vm.cellStyle = cellStyle; - vm.textColor = textColor; vm.discardAll = discardAll; vm.inputChanged = inputChanged; - vm.postData = postData; + vm.save = save; + vm.getGroupTitle = getGroupTitle; - $scope.$watch('vm.ctx', function() { + $scope.$watch('vm.ctx', function () { if (vm.ctx && vm.ctx.defaultSubscription) { vm.settings = vm.ctx.settings; vm.widgetConfig = vm.ctx.widgetConfig; @@ -72,132 +66,133 @@ function MultipleInputWidgetController($q, $scope, attributeService, toast, type } }); - $scope.$on('multiple-input-data-updated', function(event, formId) { + $scope.$on('multiple-input-data-updated', function (event, formId) { if (vm.formId == formId) { - updateRowData(vm.subscription.data); + updateWidgetData(vm.subscription.data); $scope.$digest(); } }); - function defaultStyle() { - return {}; - } - - function cellStyle(key, rowIndex, firstKey, lastKey) { - var style = {}; - if (key) { - var styleInfo = vm.stylesInfo[key.label]; - var value = key.currentValue; - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - style = styleInfo.cellStyleFunction(value); - } catch (e) { - style = {}; - } - } else { - style = defaultStyle(); - } - } - if (vm.settings.rowMargin) { - if (angular.isUndefined(style.marginTop) && rowIndex != 0) { - style.marginTop = (vm.settings.rowMargin / 2) + 'px'; - } - if (angular.isUndefined(style.marginBottom)) { - style.marginBottom = (vm.settings.rowMargin / 2) + 'px'; - } - } - if (vm.settings.columnMargin) { - if (angular.isUndefined(style.marginLeft) && !firstKey) { - style.marginLeft = (vm.settings.columnMargin / 2) + 'px'; - } - if (angular.isUndefined(style.marginRight) && !lastKey) { - style.marginRight = (vm.settings.columnMargin / 2) + 'px'; - } + $scope.$on('multiple-input-resize', function (event, formId) { + if (vm.formId == formId) { + updateWidgetDisplaying(); } - return style; - } + }); - function textColor(key) { - var style = {}; - if (key) { - var styleInfo = vm.stylesInfo[key.label]; - if (styleInfo.color) { - style = { color: styleInfo.color }; + function discardAll() { + for (var i = 0; i < vm.sources.length; i++) { + for (var j = 0; j < vm.sources[i].keys.length; j++) { + vm.sources[i].keys[j].data.currentValue = vm.sources[i].keys[j].data.originalValue; } } - return style; + $scope.multipleInputForm.$setPristine(); } - function discardAll() { - for (var r = 0; r < vm.rows.length; r++) { - var row = vm.rows[r]; - for (var d = 0; d < row.data.length; d++ ) { - row.data[d].currentValue = row.data[d].originalValue; + function inputChanged(source, key) { + if (!vm.settings.showActionButtons) { + if (!key.settings.required || (key.settings.required && key.data && angular.isDefined(key.data.currentValue))) { + var dataToSave = { + datasource: source.datasource, + keys: [key] + }; + vm.save(dataToSave); } } - vm.hasAnyChange = false; } - function inputChanged() { - var newValue = false; - for (var r = 0; r < vm.rows.length; r++) { - var row = vm.rows[r]; - for (var d = 0; d < row.data.length; d++ ) { - if (!row.data[d].currentValue) { - return; - } - if (row.data[d].currentValue !== row.data[d].originalValue) { - newValue = true; - } - } + function save(dataToSave) { + var tasks = []; + var config = { + ignoreLoading: !vm.settings.showActionButtons + }; + var data; + if (dataToSave) { + data = [dataToSave]; + } else { + data = vm.sources; } - vm.hasAnyChange = newValue; - } + for (let i = 0; i < data.length; i++) { + var serverAttributes = [], sharedAttributes = [], telemetry = []; + for (let j = 0; j < data[i].keys.length; j++) { + var key = data[i].keys[j]; + if ((key.data.currentValue !== key.data.originalValue) || vm.settings.updateAllValues) { + var attribute = { + key: key.name + }; + if (key.data.currentValue) { + switch (key.settings.dataKeyValueType) { + case 'dateTime': + case 'date': + attribute.value = key.data.currentValue.getTime(); + break; + case 'time': + attribute.value = key.data.currentValue.getTime() - moment().startOf('day').valueOf();//eslint-disable-line + break; + default: + attribute.value = key.data.currentValue; + } + } else { + if (key.data.currentValue === '') { + attribute.value = null; + } else { + attribute.value = key.data.currentValue; + } + } - function postData() { - var promises = []; - for (var r = 0; r < vm.rows.length; r++) { - var row = vm.rows[r]; - var datasource = row.datasource; - var attributes = []; - var newValues = false; - - for (var d = 0; d < row.data.length; d++ ) { - if (row.data[d].currentValue !== row.data[d].originalValue) { - attributes.push({ - key : row.data[d].name, - value : row.data[d].currentValue, - }); - newValues = true; + switch (key.settings.dataKeyType) { + case 'shared': + sharedAttributes.push(attribute); + break; + case 'timeseries': + telemetry.push(attribute); + break; + default: + serverAttributes.push(attribute); + } } } - - if (newValues) { - promises.push(attributeService.saveEntityAttributes( - datasource.entityType, - datasource.entityId, - vm.attributeScope, - attributes)); + if (serverAttributes.length) { + tasks.push(attributeService.saveEntityAttributes( + data[i].datasource.entityType, + data[i].datasource.entityId, + types.attributesScope.server.value, + serverAttributes, + config)); + } + if (sharedAttributes.length) { + tasks.push(attributeService.saveEntityAttributes( + data[i].datasource.entityType, + data[i].datasource.entityId, + types.attributesScope.shared.value, + sharedAttributes, + config)); + } + if (telemetry.length) { + tasks.push(attributeService.saveEntityTimeseries( + data[i].datasource.entityType, + data[i].datasource.entityId, + types.latestTelemetry.value, + telemetry, + config)); } } - if (promises.length) { - $q.all(promises).then( + if (tasks.length) { + $q.all(tasks).then( function success() { - for (var d = 0; d < row.data.length; d++ ) { - row.data[d].originalValue = row.data[d].currentValue; - } - vm.hasAnyChange = false; + $scope.multipleInputForm.$setPristine(); if (vm.settings.showResultMessage) { - toast.showSuccess('Update successful', 1000, angular.element(vm.ctx.$container), 'bottom left'); + toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000, angular.element(vm.ctx.$container), 'bottom left'); } }, function fail() { if (vm.settings.showResultMessage) { - toast.showError('Update failed', angular.element(vm.ctx.$container), 'bottom left'); + toast.showError($translate.instant('widgets.input-widgets.update-failed'), angular.element(vm.ctx.$container), 'bottom left'); } } ); + } else { + $scope.multipleInputForm.$setPristine(); } } @@ -211,78 +206,138 @@ function MultipleInputWidgetController($q, $scope, attributeService, toast, type vm.ctx.widgetTitle = vm.widgetTitle; - vm.attributeScope = vm.settings.attributesShared ? types.attributesScope.shared.value : types.attributesScope.server.value; - } + vm.settings.groupTitle = vm.settings.groupTitle || "${entityName}"; - function updateDatasources() { + //For backward compatibility + if (angular.isUndefined(vm.settings.showActionButtons)) { + vm.settings.showActionButtons = true; + } + if (angular.isUndefined(vm.settings.fieldsAlignment)) { + vm.settings.fieldsAlignment = 'row'; + } + if (angular.isUndefined(vm.settings.fieldsInRow)) { + vm.settings.fieldsInRow = 2; + } + //For backward compatibility - vm.stylesInfo = {}; - vm.rows = []; - vm.rowIndex = 0; + vm.isVerticalAlignment = !(vm.settings.fieldsAlignment === 'row'); - if (vm.datasources) { + if (!vm.isVerticalAlignment && vm.settings.fieldsInRow) { + vm.inputWidthSettings = 100 / vm.settings.fieldsInRow + '%'; + } + } + + function updateDatasources() { + if (vm.datasources && vm.datasources.length) { vm.entityDetected = true; - for (var ds = 0; ds < vm.datasources.length; ds++) { - var row = {}; - var datasource = vm.datasources[ds]; - row.datasource = datasource; - row.data = []; - if (datasource.dataKeys) { - vm.dataKeyDetected = true; - for (var a = 0; a < datasource.dataKeys.length; a++ ) { - var dataKey = datasource.dataKeys[a]; - - if (dataKey.units) { - dataKey.label += ' (' + dataKey.units + ')'; + for (var i = 0; i < vm.datasources.length; i++) { + var datasource = vm.datasources[i]; + var source = { + datasource: datasource, + keys: [] + }; + if (datasource.type === types.datasourceType.entity) { + for (var j = 0; j < datasource.dataKeys.length; j++) { + if ((datasource.entityType !== types.entityType.device) && (datasource.dataKeys[j].settings.dataKeyType == 'shared')) { + vm.isAllParametersValid = false; } - - var keySettings = dataKey.settings; - if (keySettings.inputTypeNumber) { - keySettings.inputType = 'number'; - } else { - keySettings.inputType = 'text'; + source.keys.push(datasource.dataKeys[j]); + if (source.keys[j].units) { + source.keys[j].label += ' (' + source.keys[j].units + ')'; + } + source.keys[j].data = {}; + + //For backward compatibility + if (angular.isUndefined(source.keys[j].settings.dataKeyType)) { + if (vm.settings.attributesShared) { + source.keys[j].settings.dataKeyType = 'shared'; + } else { + source.keys[j].settings.dataKeyType = 'server'; + } } - var cellStyleFunction = null; - var useCellStyleFunction = false; - - if (keySettings.useCellStyleFunction === true) { - if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { - try { - cellStyleFunction = new Function('value', keySettings.cellStyleFunction); - useCellStyleFunction = true; - } catch (e) { - cellStyleFunction = null; - useCellStyleFunction = false; - } + if (angular.isUndefined(source.keys[j].settings.dataKeyValueType)) { + if (source.keys[j].settings.inputTypeNumber) { + source.keys[j].settings.dataKeyValueType = 'double'; + } else { + source.keys[j].settings.dataKeyValueType = 'string'; } } - vm.stylesInfo[dataKey.label] = { - useCellStyleFunction: useCellStyleFunction, - cellStyleFunction: cellStyleFunction, - color: keySettings.color - }; + if (angular.isUndefined(source.keys[j].settings.isEditable)) { + if (source.keys[j].settings.readOnly) { + source.keys[j].settings.isEditable = 'readonly'; + } else { + source.keys[j].settings.isEditable = 'editable'; + } + } + //For backward compatibility - row.data.push(dataKey); } - vm.rows.push(row); + } else { + vm.entityDetected = false; } + vm.sources.push(source); } } } - function updateRowData(data) { + function updateWidgetData(data) { var dataIndex = 0; - for (var r = 0; r < vm.rows.length; r++) { - var row = vm.rows[r]; - for (var d = 0; d < row.data.length; d++ ) { - var keyData = data[dataIndex++].data; - if (keyData && keyData.length && keyData[0].length > 1) { - row.data[d].currentValue = row.data[d].originalValue = keyData[0][1]; + for (var i = 0; i < vm.sources.length; i++) { + var source = vm.sources[i]; + for (var j = 0; j < source.keys.length; j++) { + var keyData = data[dataIndex].data; + var key = source.keys[j]; + if (keyData && keyData.length) { + var value; + switch (key.settings.dataKeyValueType) { + case 'dateTime': + case 'date': + value = moment(keyData[0][1]).toDate(); // eslint-disable-line + break; + case 'time': + value = moment().startOf('day').add(keyData[0][1], 'ms').toDate(); // eslint-disable-line + break; + case 'booleanCheckbox': + case 'booleanSwitch': + value = (keyData[0][1] === 'true'); + break; + default: + value = keyData[0][1]; + } + + key.data = { + currentValue: value, + originalValue: value + }; + } + + if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) { + var conditions = data.filter((item) => { + return item.dataKey.name === key.settings.disabledOnDataKey; + }); + if (conditions && conditions.length) { + if (conditions[0].data.length) { + if (conditions[0].data[0][1] === 'false') { + key.settings.disabledOnCondition = true; + } else { + key.settings.disabledOnCondition = !conditions[0].data[0][1]; + } + } + } } + dataIndex++; } } } + function updateWidgetDisplaying() { + vm.changeAlignment = (vm.ctx.$container[0].offsetWidth < 620); + vm.smallWidthContainer = (vm.ctx.$container[0].offsetWidth < 420); + } + + function getGroupTitle(datasource) { + return utils.createLabelFromDatasource(datasource, vm.settings.groupTitle); + } } diff --git a/ui/src/app/widget/lib/multiple-input-widget.scss b/ui/src/app/widget/lib/multiple-input-widget.scss index 7e16b3f967..8b6d7a0fdb 100644 --- a/ui/src/app/widget/lib/multiple-input-widget.scss +++ b/ui/src/app/widget/lib/multiple-input-widget.scss @@ -15,15 +15,36 @@ */ .tb-multiple-input { height: 100%; + overflow-x: hidden; + overflow-y: auto; - .md-button.md-icon-button { - width: 32px; - min-width: 32px; - height: 32px; - min-height: 32px; - padding: 0 !important; - margin: 0; - line-height: 20px; + .fields-group { + padding: 0 8px; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + + legend { + color: rgba(0, 0, 0, .7); + } + } + + .input-field { + padding-right: 10px; + + md-input-container, + md-checkbox, + md-switch { + margin-bottom: 5px; + } + } + + md-checkbox { + margin-top: 22px; + } + + md-switch { + margin-top: 20px; + white-space: normal; } .md-icon-button md-icon { @@ -32,10 +53,56 @@ height: 20px; min-height: 20px; font-size: 20px; + } + + .date-time-input { + &__label { + margin-left: 36px; + font-size: 12px; + color: rgba(0, 0, 0, .54); + } - &:not([disabled]) { - color: #f66; + mdp-date-picker, + mdp-time-picker { + width: 100%; + + .md-button.md-icon-button { + margin: 5px 0 0; + } + + md-input-container { + width: 100%; + margin: 2px 0; + + label { + display: none; + } + } + } + } + + .vertical-alignment { + flex-direction: column; + + md-checkbox, + md-switch { + margin-top: 18px; } + + md-switch { + display: flex; + justify-content: space-between; + } + + .date-time-input { + &__label { + margin-top: 10px; + } + } + } + + .vertically-aligned { + flex-direction: column; } } diff --git a/ui/src/app/widget/lib/multiple-input-widget.tpl.html b/ui/src/app/widget/lib/multiple-input-widget.tpl.html index 1263919825..bbe55ffc13 100644 --- a/ui/src/app/widget/lib/multiple-input-widget.tpl.html +++ b/ui/src/app/widget/lib/multiple-input-widget.tpl.html @@ -15,54 +15,148 @@ limitations under the License. --> -
-
-
-
- - - - - - - {{key.settings.icon}} - - -
-
Value must be greater than {{key.settings.min}}
-
Value must be lower than {{key.settings.max}}
-
This field is required
+ +
+
+ {{ vm.getGroupTitle(source.datasource) }} + +
+
+
+ + + + {{key.settings.icon}} + + +
+
{{ key.settings.requiredErrorMessage }}
+
+
- +
+ + + + {{key.settings.icon}} + + +
+
{{ key.settings.requiredErrorMessage }}
+
+
+
+
+ + + + {{key.settings.icon}} + + +
+
{{ key.settings.requiredErrorMessage }}
+
value.invalid-integer-value
+
+
+
+
+ + {{key.label}} + +
+
+ + {{key.label}} + +
+
+ +
+ +
+
{{ key.settings.requiredErrorMessage }}
+
+
+ +
+
{{ key.settings.requiredErrorMessage }}
+
+
+
+
+
+
+
+ + {{ 'action.undo' | translate }} + + + {{ 'action.save' | translate }} +
- -
-
- No attribute is selected +
+
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }}
-
- Timeseries parameter cannot be used in this widget +
+ {{ 'widgets.input-widgets.not-allowed-entity' | translate }}
-
- - {{ 'action.undo' | translate }} - - - {{ 'action.save' | translate }} - -
diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js index 21977392bf..3d4e8b840f 100644 --- a/ui/src/app/widget/lib/openstreet-map.js +++ b/ui/src/app/widget/lib/openstreet-map.js @@ -14,18 +14,22 @@ * limitations under the License. */ import 'leaflet/dist/leaflet.css'; +import 'leaflet.markercluster/dist/MarkerCluster.css' +import 'leaflet.markercluster/dist/MarkerCluster.Default.css' import * as L from 'leaflet'; import 'leaflet-providers'; +import 'leaflet.markercluster/dist/leaflet.markercluster' export default class TbOpenStreetMap { - constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, mapProvider, credentials, defaultCenterPosition) { + constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, mapProvider, credentials, defaultCenterPosition, markerClusteringSetting) { this.utils = utils; this.defaultZoomLevel = defaultZoomLevel; this.dontFitMapBounds = dontFitMapBounds; this.minZoomLevel = minZoomLevel; this.tooltips = []; + this.isMarketCluster = markerClusteringSetting && markerClusteringSetting.isMarketCluster; if (!mapProvider) { mapProvider = { @@ -48,6 +52,11 @@ export default class TbOpenStreetMap { var tileLayer = mapProvider.isCustom ? L.tileLayer(mapProvider.name) : L.tileLayer.provider(mapProvider.name, credentials); tileLayer.addTo(this.map); + if (this.isMarketCluster) { + this.markersCluster = L.markerClusterGroup(markerClusteringSetting); + this.map.addLayer(this.markersCluster); + } + if (initCallback) { setTimeout(initCallback, 0); //eslint-disable-line } @@ -58,6 +67,10 @@ export default class TbOpenStreetMap { return angular.isDefined(this.map); } + getContainer() { + return this.isMarketCluster ? this.markersCluster : this.map; + } + updateMarkerLabel(marker, settings) { marker.unbindTooltip(); marker.bindTooltip('
' + settings.labelText + '
', @@ -137,8 +150,10 @@ export default class TbOpenStreetMap { onMarkerIconReady(iconInfo); } - createMarker(location, dsIndex, settings, onClickListener, markerArgs) { - var marker = L.marker(location, {}); + createMarker(location, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { + var marker = L.marker(location, { + draggable: settings.drraggable + }); var opMap = this; this.createMarkerIcon(marker, settings, (iconInfo) => { marker.setIcon(iconInfo.icon); @@ -147,7 +162,7 @@ export default class TbOpenStreetMap { marker.bindTooltip('
' + settings.labelText + '
', {className: 'tb-marker-label', permanent: true, direction: 'top', offset: marker.tooltipOffset}); } - marker.addTo(opMap.map); + marker.addTo(opMap.getContainer()); }); if (settings.displayTooltip) { @@ -158,11 +173,15 @@ export default class TbOpenStreetMap { marker.on('click', onClickListener); } + if (onDragendListener) { + marker.on('dragend', onDragendListener); + } + return marker; } removeMarker(marker) { - this.map.removeLayer(marker); + this.getContainer().removeLayer(marker); } createTooltip(marker, dsIndex, settings, markerArgs) { @@ -254,9 +273,9 @@ export default class TbOpenStreetMap { polygon.redraw(); } - fitBounds(bounds) { + fitBounds(bounds, useDefaultZoom) { if (bounds.isValid()) { - if (this.dontFitMapBounds && this.defaultZoomLevel) { + if ((this.dontFitMapBounds || useDefaultZoom) && this.defaultZoomLevel) { this.map.setZoom(this.defaultZoomLevel, {animate: false}); this.map.panTo(bounds.getCenter(), {animate: false}); } else { @@ -313,4 +332,8 @@ export default class TbOpenStreetMap { return this.tooltips; } + getCenter() { + return this.map.getCenter(); + } + } diff --git a/ui/src/app/widget/lib/tencent-map.js b/ui/src/app/widget/lib/tencent-map.js index ce24ad606b..9fa390cdc9 100644 --- a/ui/src/app/widget/lib/tencent-map.js +++ b/ui/src/app/widget/lib/tencent-map.js @@ -19,7 +19,7 @@ var tmGlobals = { } export default class TbTencentMap { - constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, tmApiKey, tmDefaultMapType, defaultCenterPosition) { + constructor($containerElement, utils, initCallback, defaultZoomLevel, dontFitMapBounds, disableScrollZooming, minZoomLevel, tmApiKey, tmDefaultMapType, defaultCenterPosition, markerClusteringSetting) { var tbMap = this; this.utils = utils; this.defaultZoomLevel = defaultZoomLevel; @@ -28,6 +28,7 @@ export default class TbTencentMap { this.tooltips = []; this.defaultMapType = tmDefaultMapType; this.defaultCenterPosition =defaultCenterPosition; + this.isMarketCluster = markerClusteringSetting && markerClusteringSetting.isMarketCluster; function clearGlobalId() { if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) { @@ -49,8 +50,15 @@ export default class TbTencentMap { center: new qq.maps.LatLng(tbMap.defaultCenterPosition[0],tbMap.defaultCenterPosition[1]) // eslint-disable-line no-undef }); + if (tbMap.isMarketCluster){ + tbMap.markersCluster = new qq.maps.MarkerCluster( // eslint-disable-line no-undef + angular.merge({map:tbMap.map}, markerClusteringSetting) + ); + } + + if (initCallback) { - initCallback(); + setTimeout(initCallback, 0);// eslint-disable-line } } @@ -231,14 +239,18 @@ export default class TbTencentMap { /* eslint-enable no-undef */ /* eslint-disable no-undef */ - createMarker(location, dsIndex, settings, onClickListener, markerArgs) { + createMarker(location, dsIndex, settings, onClickListener, markerArgs, onDragendListener) { var marker = new qq.maps.Marker({ position: location }); var tMap = this; this.createMarkerIcon(marker, settings, (iconInfo) => { marker.setIcon(iconInfo.icon); - marker.setMap(tMap.map); + if(tMap.isMarketCluster) { + tMap.markersCluster.addMarker(marker); + } else { + marker.setMap(tMap.map); + } if (settings.showLabel) { marker.label = new qq.maps.Label({ clickable: false, @@ -248,7 +260,8 @@ export default class TbTencentMap { visible: true, position: location, map: tMap.map, - zIndex: 1000 + zIndex: 1000, + draggable: settings.drraggable }); } }); @@ -261,6 +274,10 @@ export default class TbTencentMap { qq.maps.event.addListener(marker, 'click', onClickListener); } + if (onDragendListener) { + qq.maps.event.addListener(marker, 'dragend', onDragendListener); + } + return marker; } @@ -410,8 +427,8 @@ export default class TbTencentMap { } /* eslint-disable no-undef ,no-unused-vars*/ - fitBounds(bounds) { - if (this.dontFitMapBounds && this.defaultZoomLevel) { + fitBounds(bounds, useDefaultZoom) { + if ((this.dontFitMapBounds || useDefaultZoom) && this.defaultZoomLevel) { this.map.setZoom(this.defaultZoomLevel); this.map.setCenter(bounds.getCenter()); } else { @@ -475,4 +492,8 @@ export default class TbTencentMap { return this.tooltips; } + getCenter() { + return this.map.getCenter(); + } + } diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js index 10fc548f5a..b470be72f9 100644 --- a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js +++ b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js @@ -128,7 +128,8 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt vm.index = 0; vm.dsIndex = 0; vm.minTime = 0; - vm.maxTime = 0; + vm.minTimeIndex = 0; + vm.maxTimeIndex = 0; vm.isPlaying = false; vm.trackingLine = { "type": "FeatureCollection", @@ -200,10 +201,10 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt vm.moveNext = function () { vm.stopPlay(); if (vm.staticSettings.usePointAsAnchor) { - let newIndex = vm.maxTime; - for (let index = vm.index+1; index < vm.maxTime; index++) { + let newIndex = vm.maxTimeIndex; + for (let index = vm.index + 1; index < vm.maxTimeIndex; index++) { if (vm.trips.some(function (trip) { - return trip.timeRange[index].hasAnchor; + return calculateCurrentDate(trip.timeRange, index).hasAnchor; })) { newIndex = index; break; @@ -216,27 +217,27 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt vm.movePrev = function () { vm.stopPlay(); if (vm.staticSettings.usePointAsAnchor) { - let newIndex = vm.minTime; - for (let index = vm.index-1; index > vm.minTime; index--) { + let newIndex = vm.minTimeIndex; + for (let index = vm.index - 1; index > vm.minTimeIndex; index--) { if (vm.trips.some(function (trip) { - return trip.timeRange[index].hasAnchor; - })) { + return calculateCurrentDate(trip.timeRange, index).hasAnchor; + })) { newIndex = index; break; } } moveToIndex(newIndex); - } else moveInc(-1); + } else moveInc(-1); }; vm.moveStart = function () { vm.stopPlay(); - moveToIndex(vm.minTime); + moveToIndex(vm.minTimeIndex); }; vm.moveEnd = function () { vm.stopPlay(); - moveToIndex(vm.maxTime); + moveToIndex(vm.maxTimeIndex); }; vm.stopPlay = function () { @@ -252,8 +253,9 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt } function moveToIndex(newIndex) { - if (newIndex > vm.maxTime || newIndex < vm.minTime) return; + if (newIndex > vm.maxTimeIndex || newIndex < vm.minTimeIndex) return; vm.index = newIndex; + vm.animationTime = vm.minTime + vm.index * vm.staticSettings.normalizationStep; recalculateTrips(); } @@ -263,12 +265,6 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt }) } - function findAngle(lat1, lng1, lat2, lng2) { - let angle = Math.atan2(0, 0) - Math.atan2(lat2 - lat1, lng2 - lng1); - angle = angle * 180 / Math.PI; - return parseInt(angle.toFixed(2)); - } - function initialize() { $scope.currentDate = $filter('date')(0, "yyyy.MM.dd HH:mm:ss"); @@ -445,7 +441,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt } } - function configureTripSettings(trip, index, apply) { + function configureTripSettings(trip, apply) { trip.settings = {}; trip.settings.color = calculateColor(trip); trip.settings.polygonColor = calculatePolygonColor(trip); @@ -478,17 +474,17 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt let labelText = vm.staticSettings.label; if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) { try { - labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dsIndex); + labelText = vm.staticSettings.labelFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dsIndex); } catch (e) { labelText = null; } } labelText = vm.utils.createLabelFromDatasource(trip.dataSource, labelText); labelReplaceInfo = processPattern(labelText, vm.ctx.datasources, trip.dSIndex); - label = fillPattern(labelText, labelReplaceInfo, trip.timeRange[vm.index]); + label = fillPattern(labelText, labelReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index)); if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) { try { - labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex); + labelText = vm.staticSettings.labelFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); } catch (e) { labelText = null; } @@ -504,14 +500,14 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt let tooltipText = vm.staticSettings.tooltipPattern; if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) { try { - tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex); + tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); } catch (e) { tooltipText = null; } } tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText); tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex); - tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[vm.index]); + tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index)); tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null); } @@ -525,14 +521,14 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt let tooltipText = vm.staticSettings.polygonTooltipPattern; if (vm.staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.staticSettings.polygonTooltipFunction)) { try { - tooltipText = vm.staticSettings.polygonTooltipFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex); + tooltipText = vm.staticSettings.polygonTooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); } catch (e) { tooltipText = null; } } tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText); tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex); - tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[vm.index]); + tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, vm.index)); tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null); } @@ -546,14 +542,14 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt let tooltipText = vm.staticSettings.tooltipPattern; if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) { try { - tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, trip.timeRange[index], trip.dSIndex); + tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, index), trip.dSIndex); } catch (e) { tooltipText = null; } } tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText); tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex); - tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[index]); + tooltip = fillPattern(tooltipText, tooltipReplaceInfo, calculateCurrentDate(trip.timeRange, index)); tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null); } @@ -565,7 +561,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt let colorFn; if (vm.staticSettings.usePathColorFunction && angular.isDefined(vm.staticSettings.colorFunction)) { try { - colorFn = vm.staticSettings.colorFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex); + colorFn = vm.staticSettings.colorFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); } catch (e) { colorFn = null; } @@ -581,7 +577,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt let colorFn; if (vm.staticSettings.usePolygonColorFunction && angular.isDefined(vm.staticSettings.polygonColorFunction)) { try { - colorFn = vm.staticSettings.polygonColorFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex); + colorFn = vm.staticSettings.polygonColorFunction(vm.ctx.data, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); } catch (e) { colorFn = null; } @@ -597,7 +593,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt if (vm.staticSettings.useMarkerImageFunction && angular.isDefined(vm.staticSettings.markerImageFunction)) { let rawIcon; try { - rawIcon = vm.staticSettings.markerImageFunction(vm.ctx.data, vm.staticSettings.markerImages, trip.timeRange[vm.index], trip.dSIndex); + rawIcon = vm.staticSettings.markerImageFunction(vm.ctx.data, vm.staticSettings.markerImages, calculateCurrentDate(trip.timeRange, vm.index), trip.dSIndex); } catch (e) { rawIcon = null; } @@ -658,8 +654,8 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt }); vm.initBounds = true; } - let normalizedTimeRange = createNormalizedTime(vm.data, vm.staticSettings.normalizationStep); - createNormalizedTrips(normalizedTimeRange, vm.datasources); + createNormalizedTime(vm.data, vm.staticSettings.normalizationStep); + createNormalizedTrips(vm.datasources, vm.data, vm.staticSettings.normalizationStep); createTripsOnMap(apply); if (vm.initBounds && !vm.initTrips) { vm.trips.forEach(function (trip) { @@ -701,127 +697,82 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt function createNormalizedTime(data, step) { if (!step) step = 1000; - let max_time = null; - let min_time = null; - let normalizedArray = []; - if (data && data.length > 0) { - vm.data.forEach(function (data) { - if (data.data.length > 0) { - data.data.forEach(function (sData) { - if (max_time === null) { - max_time = sData[0]; - } else if (max_time < sData[0]) { - max_time = sData[0] - } - if (min_time === null) { - min_time = sData[0]; - } else if (min_time > sData[0]) { - min_time = sData[0]; - } - }) + let max_time = -Infinity; + let min_time = Infinity; + if (data) { + for (let i = 0; i < data.length; i++) { + for (let j = 0; j < data[i].data.length; j++) { + if (max_time < data[i].data[j][0]) { + max_time = data[i].data[j][0] + } + if (min_time > data[i].data[j][0]) { + min_time = data[i].data[j][0]; + } } - }); - for (let i = min_time; i < max_time; i += step) { - normalizedArray.push({ts: i, formattedTs: $filter('date')(i, 'medium')}); - - } - if (normalizedArray[normalizedArray.length - 1] && normalizedArray[normalizedArray.length - 1].ts !== max_time) { - normalizedArray.push({ts: max_time, formattedTs: $filter('date')(max_time, 'medium')}); } } - vm.maxTime = normalizedArray.length - 1; - //vm.minTime = vm.maxTime > 1 ? 1 : 0; - if (vm.index < vm.minTime) { - vm.index = vm.minTime; - } else if (vm.index > vm.maxTime) { - vm.index = vm.maxTime; + vm.minTime = vm.animationTime = min_time; + if(min_time === Infinity){ + vm.animationTime = null; + } else { + vm.animationTime = min_time + } + vm.maxTimeIndex = Math.ceil((max_time - min_time) / step); + if (vm.index < vm.minTimeIndex) { + vm.index = vm.minTimeIndex; + } else if (vm.index > vm.maxTimeIndex) { + vm.index = vm.maxTimeIndex; } - return normalizedArray; } - function createNormalizedTrips(timeRange, dataSources) { + function createNormalizedTrips(dataSources, data, step) { vm.trips = []; - if (timeRange && timeRange.length > 0 && dataSources && dataSources.length > 0 && vm.data && vm.data.length > 0) { - dataSources.forEach(function (dS, index) { + step = step || 1000; + if (dataSources && data) { + for (let i = 0; i < dataSources.length; i++) { vm.trips.push({ - dataSource: dS, - dSIndex: index, - timeRange: angular.copy(timeRange) + dataSource: dataSources[i], + dSIndex: i, + timeRange: {} }) - }); + } - vm.data.forEach(function (data) { - let ds = data.datasource; + for (let i = 0; i < data.length; i++) { + let ds = data[i].datasource; let tripIndex = vm.trips.findIndex(function (el) { return el.dataSource.entityId === ds.entityId; }); - if (tripIndex > -1) { - createNormalizedValue(data.data, data.dataKey.label, vm.trips[tripIndex].timeRange); + createNormalizedValue(data[i].data, data[i].dataKey.label, vm.trips[tripIndex].timeRange, step); } - }) + } } createNormalizedLatLngs(); } - function createNormalizedValue(dataArray, dataKey, timeRangeArray) { - timeRangeArray.forEach(function (timeStamp) { - let targetTDiff = null; - let value = null; - for (let i = 0; i < dataArray.length; i++) { - let tDiff = dataArray[i][0] - timeStamp.ts; - if (targetTDiff === null || (tDiff <= 0 && targetTDiff < tDiff)) { - targetTDiff = tDiff; - value = dataArray[i][1]; - - } - } - if (value !== null) timeStamp[dataKey] = value; - }); + function createNormalizedValue(dataArray, dataKey, timeRange, step) { + for (let i = 0; i < dataArray.length; i++) { + let normalizeTime = vm.minTime + Math.ceil((dataArray[i][0] - vm.minTime) / step) * step; + timeRange[normalizeTime] = timeRange[normalizeTime] || {}; + timeRange[normalizeTime][dataKey] = dataArray[i][1]; + } } function createNormalizedLatLngs() { - vm.trips.forEach(function (el) { - el.latLngs = []; - el.timeRange.forEach(function (data) { - let lat = data[vm.staticSettings.latKeyName]; - let lng = data[vm.staticSettings.lngKeyName]; - if (lat && lng && vm.map) { - data.latLng = vm.map.createLatLng(lat, lng); - } - el.latLngs.push(data.latLng); - }); - addAngleForTrip(el); - }) - } - - function addAngleForTrip(trip) { - if (trip.timeRange && trip.timeRange.length > 0) { - trip.timeRange.forEach(function (point, index) { - let nextPoint, prevPoint; - nextPoint = index === (trip.timeRange.length - 1) ? trip.timeRange[index] : trip.timeRange[index + 1]; - prevPoint = index === 0 ? trip.timeRange[0] : trip.timeRange[index - 1]; - let nextLatLng = { - lat: nextPoint[vm.staticSettings.latKeyName], - lng: nextPoint[vm.staticSettings.lngKeyName] - }; - let prevLatLng = { - lat: prevPoint[vm.staticSettings.latKeyName], - lng: prevPoint[vm.staticSettings.lngKeyName] - }; - if (nextLatLng.lat === prevLatLng.lat && nextLatLng.lng === prevLatLng.lng) { - if (angular.isNumber(prevPoint.h)) { - point.h = prevPoint.h; - } else { - point.h = vm.staticSettings.rotationAngle; + vm.trips.forEach(function (item) { + item.latLngs = []; + for (let timestamp in item.timeRange) { + if(Object.prototype.hasOwnProperty.call(item.timeRange, timestamp)) { + let lat = item.timeRange[timestamp][vm.staticSettings.latKeyName]; + let lng = item.timeRange[timestamp][vm.staticSettings.lngKeyName]; + if (lat && lng && vm.map) { + item.timeRange[timestamp].latLng = vm.map.createLatLng(lat, lng); } - } else { - point.h = findAngle(prevLatLng.lat, prevLatLng.lng, nextLatLng.lat, nextLatLng.lng); - point.h += vm.staticSettings.rotationAngle; + item.latLngs.push(item.timeRange[timestamp].latLng); } - }); - } + } + }); } function createPointPopup(point, index, trip) { @@ -834,14 +785,14 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt function createTripsOnMap(apply) { if (vm.trips.length > 0) { vm.trips.forEach(function (trip) { - configureTripSettings(trip, vm.index, apply); - if (trip.timeRange.length > 0 && trip.latLngs.every(el => angular.isDefined(el))) { + configureTripSettings(trip, apply); + if (Object.keys(trip.timeRange).length > 0 && trip.latLngs.every(el => angular.isDefined(el))) { if (vm.staticSettings.showPoints) { trip.points = []; - trip.timeRange.forEach(function (tRange, index) { - if (tRange && tRange.latLng - && (!vm.staticSettings.usePointAsAnchor || vm.staticSettings.pointAsAnchorFunction(vm.ctx.data, tRange, trip.dSIndex))) { - let point = L.circleMarker(tRange.latLng, { + Object.keys(trip.timeRange).forEach(function (tRange, index) { + if (trip.timeRange[tRange] && trip.timeRange[tRange].latLng + && (!vm.staticSettings.usePointAsAnchor || vm.staticSettings.pointAsAnchorFunction(vm.ctx.data, trip.timeRange[tRange], trip.dSIndex))) { + let point = L.circleMarker(trip.timeRange[tRange].latLng, { color: trip.settings.pointColor, radius: trip.settings.pointSize }).addTo(vm.map.map); @@ -852,7 +803,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt showHidePointTooltip(calculatePointTooltip(trip, index), index); }); } - if (vm.staticSettings.usePointAsAnchor) tRange.hasAnchor = true; + if (vm.staticSettings.usePointAsAnchor) trip.timeRange[tRange].hasAnchor = true; trip.points.push(point); } }); @@ -872,7 +823,8 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt polygon: false, pathOptions: { color: vm.staticSettings.useDecoratorCustomColor ? vm.staticSettings.decoratorCustomColor : trip.settings.color, - stroke: true} + stroke: true + } }) } ], @@ -882,8 +834,8 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt } - if (trip.timeRange && trip.timeRange.length && angular.isUndefined(trip.marker)) { - trip.marker = L.marker(trip.timeRange[vm.index].latLng); + if (trip.timeRange && Object.keys(trip.timeRange).length && angular.isUndefined(trip.marker)) { + trip.marker = L.marker(calculateCurrentDate(trip.timeRange, vm.index).latLng); trip.marker.setZIndexOffset(1000); trip.marker.setIcon(vm.staticSettings.icon); trip.marker.setRotationOrigin('center center'); @@ -895,7 +847,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt } } - if (vm.staticSettings.showPolygon && angular.isDefined(trip.timeRange[vm.index][vm.staticSettings.polKeyName])) { + if (vm.staticSettings.showPolygon && angular.isDefined(calculateCurrentDate(trip.timeRange, vm.index)[vm.staticSettings.polKeyName])) { let polygonSettings = { fill: true, fillColor: trip.settings.polygonColor, @@ -904,7 +856,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt fillOpacity: trip.settings.polygonOpacity, opacity: trip.settings.polygonStrokeOpacity }; - let polygonLatLngsRaw = mapPolygonArray(angular.fromJson(trip.timeRange[vm.index][vm.staticSettings.polKeyName])); + let polygonLatLngsRaw = mapPolygonArray(angular.fromJson(calculateCurrentDate(trip.timeRange, vm.index)[vm.staticSettings.polKeyName])); trip.polygon = L.polygon(polygonLatLngsRaw, polygonSettings).addTo(vm.map.map); trip.polygon.on('click',function(){showHidePolygonTooltip(trip)}); } @@ -912,6 +864,35 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt } } + function calculateCurrentDate(tripTimeRange, index) { + let time = vm.minTime + index * vm.staticSettings.normalizationStep; + if (Object.hasOwnProperty.call(tripTimeRange, time)) { + return tripTimeRange[time]; + } else { + let timeInterval = Object.keys(tripTimeRange); + for (let i = 1; i < timeInterval.length; i++) { + if (timeInterval[i - 1] < time && timeInterval[i] > time) { + let calcPosition = angular.copy(tripTimeRange[timeInterval[i - 1]]); + let startLatLng = tripTimeRange[timeInterval[i - 1]].latLng; + let finishLatLng = tripTimeRange[timeInterval[i]].latLng; + let percentRouteComplete = (time - timeInterval[i - 1]) / (timeInterval[i] - timeInterval[i - 1]); + calcPosition[vm.staticSettings.latKeyName] = startLatLng.lat + (finishLatLng.lat - startLatLng.lat) * percentRouteComplete; + calcPosition[vm.staticSettings.lngKeyName] = startLatLng.lng + (finishLatLng.lng - startLatLng.lng) * percentRouteComplete; + calcPosition.latLng = vm.map.createLatLng(calcPosition[vm.staticSettings.latKeyName], calcPosition[vm.staticSettings.lngKeyName]); + calcPosition.angle = vm.staticSettings.rotationAngle + findAngle(startLatLng, finishLatLng); + return calcPosition; + } + } + } + return {}; + } + + function findAngle(startPoint, endPoint) { + let angle = -Math.atan2(endPoint.lat - startPoint.lat, endPoint.lng - startPoint.lng); + angle = angle * 180 / Math.PI; + return parseInt(angle.toFixed(2)); + } + function mapPolygonArray(rawArray) { return rawArray.map(function (el) { if (el.length === 2) { @@ -931,15 +912,16 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt } function moveMarker(trip) { - if (angular.isDefined(trip.timeRange[vm.index].latLng)) { + let postionMarker = calculateCurrentDate(trip.timeRange, vm.index); + if (angular.isDefined(postionMarker)) { if (angular.isDefined(trip.marker)) { trip.markerAngleIsSet = true; - trip.marker.setLatLng(trip.timeRange[vm.index].latLng); - trip.marker.setRotationAngle(trip.timeRange[vm.index].h); + trip.marker.setLatLng(postionMarker.latLng); + trip.marker.setRotationAngle(postionMarker.angle); trip.marker.update(); } else { if (trip.timeRange && trip.timeRange.length) { - trip.marker = L.marker(trip.timeRange[vm.index].latLng); + trip.marker = L.marker(postionMarker.latLng); trip.marker.setZIndexOffset(1000); trip.marker.setIcon(vm.staticSettings.icon); trip.marker.setRotationOrigin('center center'); @@ -1000,4 +982,4 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex; vm.mainTooltip = vm.trips[vm.activeTripIndex].settings.polygonTooltipText; } -} \ No newline at end of file +} diff --git a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html index 49c19b96d9..ce2d6e324d 100644 --- a/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html +++ b/ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html @@ -27,8 +27,10 @@
-
+
@@ -39,7 +41,7 @@ skip_previous - + skip_next @@ -61,7 +63,9 @@ -
{{vm.trips[vm.activeTripIndex].timeRange[vm.index].ts | date:'medium'}} +
+ {{ vm.animationTime | date:'medium'}} + {{ "widget.no-data-found" | translate}}