Commit 9b4309c61a5349a7ea6c7bf45a5d69ccc34a524a

Authored by Vladyslav_Prykhodko
2 parents 79814877 b6299623

Merge remote-tracking branch 'upstream/master' into develop/3.3

Showing 100 changed files with 2406 additions and 329 deletions

Too many changes to show.

To preserve performance only 100 of 102 files are displayed.

@@ -26,22 +26,6 @@ @@ -26,22 +26,6 @@
26 } 26 }
27 }, 27 },
28 { 28 {
29 - "alias": "basic_timeseries",  
30 - "name": "Timeseries - Flot",  
31 - "descriptor": {  
32 - "type": "timeseries",  
33 - "sizeX": 8,  
34 - "sizeY": 5,  
35 - "resources": [],  
36 - "templateHtml": "",  
37 - "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",  
38 - "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",  
39 - "settingsSchema": "{}",  
40 - "dataKeySettingsSchema": "{}",  
41 - "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}"  
42 - }  
43 - },  
44 - {  
45 "alias": "doughnut_chart_js", 29 "alias": "doughnut_chart_js",
46 "name": "Doughnut - Chart.js", 30 "name": "Doughnut - Chart.js",
47 "descriptor": { 31 "descriptor": {
@@ -71,7 +55,7 @@ @@ -71,7 +55,7 @@
71 "resources": [], 55 "resources": [],
72 "templateHtml": "", 56 "templateHtml": "",
73 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n", 57 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n",
74 - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", 58 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n",
75 "settingsSchema": "{}\n", 59 "settingsSchema": "{}\n",
76 "dataKeySettingsSchema": "{}\n", 60 "dataKeySettingsSchema": "{}\n",
77 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" 61 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
@@ -138,8 +122,8 @@ @@ -138,8 +122,8 @@
138 } 122 }
139 }, 123 },
140 { 124 {
141 - "alias": "timeseries_bars_flot",  
142 - "name": "Timeseries Bars - Flot", 125 + "alias": "state_chart",
  126 + "name": "State Chart",
143 "descriptor": { 127 "descriptor": {
144 "type": "timeseries", 128 "type": "timeseries",
145 "sizeX": 8, 129 "sizeX": 8,
@@ -147,15 +131,15 @@ @@ -147,15 +131,15 @@
147 "resources": [], 131 "resources": [],
148 "templateHtml": "", 132 "templateHtml": "",
149 "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", 133 "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",
150 - "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", 134 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
151 "settingsSchema": "{}", 135 "settingsSchema": "{}",
152 "dataKeySettingsSchema": "{}", 136 "dataKeySettingsSchema": "{}",
153 - "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\":{}}" 137 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
154 } 138 }
155 }, 139 },
156 { 140 {
157 - "alias": "state_chart",  
158 - "name": "State Chart", 141 + "alias": "basic_timeseries",
  142 + "name": "Timeseries - Flot",
159 "descriptor": { 143 "descriptor": {
160 "type": "timeseries", 144 "type": "timeseries",
161 "sizeX": 8, 145 "sizeX": 8,
@@ -163,11 +147,27 @@ @@ -163,11 +147,27 @@
163 "resources": [], 147 "resources": [],
164 "templateHtml": "", 148 "templateHtml": "",
165 "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", 149 "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",
166 - "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", 150 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
167 "settingsSchema": "{}", 151 "settingsSchema": "{}",
168 "dataKeySettingsSchema": "{}", 152 "dataKeySettingsSchema": "{}",
169 - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" 153 + "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}"
  154 + }
  155 + },
  156 + {
  157 + "alias": "timeseries_bars_flot",
  158 + "name": "Timeseries Bars - Flot",
  159 + "descriptor": {
  160 + "type": "timeseries",
  161 + "sizeX": 8,
  162 + "sizeY": 5,
  163 + "resources": [],
  164 + "templateHtml": "",
  165 + "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",
  166 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  167 + "settingsSchema": "{}",
  168 + "dataKeySettingsSchema": "{}",
  169 + "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\":{}}"
170 } 170 }
171 } 171 }
172 ] 172 ]
173 -} 173 +}
  1 +{
  2 + "widgetsBundle": {
  3 + "alias": "navigation_widgets",
  4 + "title": "Navigation widgets",
  5 + "image": null
  6 + },
  7 + "widgetTypes": [
  8 + {
  9 + "alias": "navigation_cards",
  10 + "name": "Navigation cards",
  11 + "descriptor": {
  12 + "type": "static",
  13 + "sizeX": 7,
  14 + "sizeY": 6,
  15 + "resources": [],
  16 + "templateHtml": "<tb-navigation-cards-widget [ctx]=\"ctx\"></tb-navigation-cards-widget>",
  17 + "templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}",
  18 + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n",
  19 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"filterType\": {\n \"title\": \"Filter type\",\n \"type\": \"string\",\n \"default\": \"all\"\n },\n \"filter\": {\n \"title\": \"Items\",\n \"type\": \"array\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"filterType\",\n \"type\": \"radios\",\n \"direction\": \"row\",\n \"titleMap\": [\n {\n \"value\": \"all\",\n \"name\": \"All items\"\n },\n {\n \"value\": \"include\",\n \"name\": \"Include items\"\n },\n {\n \"value\": \"exclude\",\n \"name\": \"Exclude items\"\n }\n ]\n },\n {\n \"key\": \"filter\",\n \"type\": \"rc-select\",\n \"condition\": \"model.filterType !== 'all'\",\n \"tags\": true,\n \"placeholder\": \"Enter urls to filter\",\n \"items\": [{\"value\": \"/devices\", \"label\": \"/devices\"}, {\"value\": \"/assets\", \"label\": \"/assets\"}, {\"value\": \"/deviceProfies\", \"label\": \"/deviceProfies\"}]\n }\n ]\n}\n",
  20 + "dataKeySettingsSchema": "{}\n",
  21 + "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\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
  22 + }
  23 + },
  24 + {
  25 + "alias": "navigation_card",
  26 + "name": "Navigation card",
  27 + "descriptor": {
  28 + "type": "static",
  29 + "sizeX": 2.5,
  30 + "sizeY": 2,
  31 + "resources": [],
  32 + "templateHtml": "<tb-navigation-card-widget [ctx]=\"ctx\"></tb-navigation-card-widget>",
  33 + "templateCss": "",
  34 + "controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n",
  35 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"name\": {\n \"title\": \"Title\",\n \"type\": \"string\",\n \"default\": \"{i18n:device.devices}\"\n },\n \"icon\": {\n \"title\": \"icon\",\n \"type\": \"string\",\n \"default\": \"devices_other\"\n },\n \"path\": {\n \"title\": \"Navigation path\",\n \"type\": \"string\",\n \"default\": \"/devices\"\n }\n },\n \"required\": [\"name\", \"icon\", \"path\"]\n },\n \"form\": [\n \"name\",\n {\n \"key\": \"icon\",\n \"type\": \"icon\"\n },\n \"path\"\n ]\n}\n",
  36 + "dataKeySettingsSchema": "{}\n",
  37 + "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\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
  38 + }
  39 + }
  40 + ]
  41 +}
@@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 import org.springframework.beans.factory.annotation.Value; 20 import org.springframework.beans.factory.annotation.Value;
19 import org.springframework.http.HttpStatus; 21 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 22 import org.springframework.security.access.prepost.PreAuthorize;
@@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer; @@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer;
30 import org.thingsboard.server.common.data.Dashboard; 32 import org.thingsboard.server.common.data.Dashboard;
31 import org.thingsboard.server.common.data.DashboardInfo; 33 import org.thingsboard.server.common.data.DashboardInfo;
32 import org.thingsboard.server.common.data.EntityType; 34 import org.thingsboard.server.common.data.EntityType;
  35 +import org.thingsboard.server.common.data.HomeDashboard;
  36 +import org.thingsboard.server.common.data.HomeDashboardInfo;
33 import org.thingsboard.server.common.data.ShortCustomerInfo; 37 import org.thingsboard.server.common.data.ShortCustomerInfo;
  38 +import org.thingsboard.server.common.data.Tenant;
  39 +import org.thingsboard.server.common.data.User;
34 import org.thingsboard.server.common.data.audit.ActionType; 40 import org.thingsboard.server.common.data.audit.ActionType;
35 import org.thingsboard.server.common.data.exception.ThingsboardException; 41 import org.thingsboard.server.common.data.exception.ThingsboardException;
36 import org.thingsboard.server.common.data.id.CustomerId; 42 import org.thingsboard.server.common.data.id.CustomerId;
@@ -39,7 +45,9 @@ import org.thingsboard.server.common.data.id.TenantId; @@ -39,7 +45,9 @@ import org.thingsboard.server.common.data.id.TenantId;
39 import org.thingsboard.server.common.data.page.PageData; 45 import org.thingsboard.server.common.data.page.PageData;
40 import org.thingsboard.server.common.data.page.PageLink; 46 import org.thingsboard.server.common.data.page.PageLink;
41 import org.thingsboard.server.common.data.page.TimePageLink; 47 import org.thingsboard.server.common.data.page.TimePageLink;
  48 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
42 import org.thingsboard.server.queue.util.TbCoreComponent; 49 import org.thingsboard.server.queue.util.TbCoreComponent;
  50 +import org.thingsboard.server.service.security.model.SecurityUser;
43 import org.thingsboard.server.service.security.permission.Operation; 51 import org.thingsboard.server.service.security.permission.Operation;
44 import org.thingsboard.server.service.security.permission.Resource; 52 import org.thingsboard.server.service.security.permission.Resource;
45 53
@@ -53,6 +61,9 @@ public class DashboardController extends BaseController { @@ -53,6 +61,9 @@ public class DashboardController extends BaseController {
53 61
54 public static final String DASHBOARD_ID = "dashboardId"; 62 public static final String DASHBOARD_ID = "dashboardId";
55 63
  64 + private static final String HOME_DASHBOARD_ID = "homeDashboardId";
  65 + private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
  66 +
56 @Value("${dashboard.max_datapoints_limit}") 67 @Value("${dashboard.max_datapoints_limit}")
57 private long maxDatapointsLimit; 68 private long maxDatapointsLimit;
58 69
@@ -472,4 +483,100 @@ public class DashboardController extends BaseController { @@ -472,4 +483,100 @@ public class DashboardController extends BaseController {
472 throw handleException(e); 483 throw handleException(e);
473 } 484 }
474 } 485 }
  486 +
  487 + @PreAuthorize("isAuthenticated()")
  488 + @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET)
  489 + @ResponseBody
  490 + public HomeDashboard getHomeDashboard() throws ThingsboardException {
  491 + try {
  492 + SecurityUser securityUser = getCurrentUser();
  493 + if (securityUser.isSystemAdmin()) {
  494 + return null;
  495 + }
  496 + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
  497 + JsonNode additionalInfo = user.getAdditionalInfo();
  498 + HomeDashboard homeDashboard;
  499 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  500 + if (homeDashboard == null) {
  501 + if (securityUser.isCustomerUser()) {
  502 + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId());
  503 + additionalInfo = customer.getAdditionalInfo();
  504 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  505 + }
  506 + if (homeDashboard == null) {
  507 + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId());
  508 + additionalInfo = tenant.getAdditionalInfo();
  509 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  510 + }
  511 + }
  512 + return homeDashboard;
  513 + } catch (Exception e) {
  514 + throw handleException(e);
  515 + }
  516 + }
  517 +
  518 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  519 + @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET)
  520 + @ResponseBody
  521 + public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException {
  522 + try {
  523 + Tenant tenant = tenantService.findTenantById(getTenantId());
  524 + JsonNode additionalInfo = tenant.getAdditionalInfo();
  525 + DashboardId dashboardId = null;
  526 + boolean hideDashboardToolbar = true;
  527 + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
  528 + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
  529 + dashboardId = new DashboardId(toUUID(strDashboardId));
  530 + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
  531 + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
  532 + }
  533 + }
  534 + return new HomeDashboardInfo(dashboardId, hideDashboardToolbar);
  535 + } catch (Exception e) {
  536 + throw handleException(e);
  537 + }
  538 + }
  539 +
  540 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  541 + @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST)
  542 + @ResponseStatus(value = HttpStatus.OK)
  543 + public void setTenantHomeDashboardInfo(@RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException {
  544 + try {
  545 + if (homeDashboardInfo.getDashboardId() != null) {
  546 + checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ);
  547 + }
  548 + Tenant tenant = tenantService.findTenantById(getTenantId());
  549 + JsonNode additionalInfo = tenant.getAdditionalInfo();
  550 + if (additionalInfo == null || !(additionalInfo instanceof ObjectNode)) {
  551 + additionalInfo = JacksonUtil.OBJECT_MAPPER.createObjectNode();
  552 + }
  553 + if (homeDashboardInfo.getDashboardId() != null) {
  554 + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_ID, homeDashboardInfo.getDashboardId().getId().toString());
  555 + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_HIDE_TOOLBAR, homeDashboardInfo.isHideDashboardToolbar());
  556 + } else {
  557 + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_ID);
  558 + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_HIDE_TOOLBAR);
  559 + }
  560 + tenant.setAdditionalInfo(additionalInfo);
  561 + tenantService.saveTenant(tenant);
  562 + } catch (Exception e) {
  563 + throw handleException(e);
  564 + }
  565 + }
  566 +
  567 + private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) {
  568 + try {
  569 + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
  570 + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
  571 + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
  572 + Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
  573 + boolean hideDashboardToolbar = true;
  574 + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
  575 + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
  576 + }
  577 + return new HomeDashboard(dashboard, hideDashboardToolbar);
  578 + }
  579 + } catch (Exception e) {}
  580 + return null;
  581 + }
475 } 582 }
@@ -185,6 +185,8 @@ public class ThingsboardInstallService { @@ -185,6 +185,8 @@ public class ThingsboardInstallService {
185 case "3.2.0": 185 case "3.2.0":
186 log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ..."); 186 log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
187 databaseEntitiesUpgradeService.upgradeDatabase("3.2.0"); 187 databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
  188 + case "3.2.1":
  189 + log.info("Upgrading ThingsBoard from version 3.2.1 to 3.3.0 ...");
188 log.info("Updating system data..."); 190 log.info("Updating system data...");
189 systemDataLoaderService.updateSystemWidgets(); 191 systemDataLoaderService.updateSystemWidgets();
190 break; 192 break;
@@ -197,7 +197,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @@ -197,7 +197,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
197 generalSettings.setKey("general"); 197 generalSettings.setKey("general");
198 ObjectNode node = objectMapper.createObjectNode(); 198 ObjectNode node = objectMapper.createObjectNode();
199 node.put("baseUrl", "http://localhost:8080"); 199 node.put("baseUrl", "http://localhost:8080");
200 - node.put("prohibitDifferentUrl", true); 200 + node.put("prohibitDifferentUrl", false);
201 generalSettings.setJsonValue(node); 201 generalSettings.setJsonValue(node);
202 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings); 202 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
203 203
@@ -438,6 +438,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @@ -438,6 +438,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
438 this.deleteSystemWidgetBundle("input_widgets"); 438 this.deleteSystemWidgetBundle("input_widgets");
439 this.deleteSystemWidgetBundle("date"); 439 this.deleteSystemWidgetBundle("date");
440 this.deleteSystemWidgetBundle("entity_admin_widgets"); 440 this.deleteSystemWidgetBundle("entity_admin_widgets");
  441 + this.deleteSystemWidgetBundle("navigation_widgets");
441 installScripts.loadSystemWidgets(); 442 installScripts.loadSystemWidgets();
442 } 443 }
443 444
@@ -15,11 +15,20 @@ @@ -15,11 +15,20 @@
15 */ 15 */
16 package org.thingsboard.server.service.install; 16 package org.thingsboard.server.service.install;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 import org.springframework.context.annotation.Profile; 19 import org.springframework.context.annotation.Profile;
19 import org.springframework.stereotype.Service; 20 import org.springframework.stereotype.Service;
20 import org.thingsboard.server.dao.util.HsqlDao; 21 import org.thingsboard.server.dao.util.HsqlDao;
21 22
  23 +import java.nio.charset.Charset;
  24 +import java.nio.file.Files;
  25 +import java.nio.file.Path;
  26 +import java.nio.file.Paths;
  27 +import java.sql.Connection;
  28 +import java.sql.DriverManager;
  29 +
22 @Service 30 @Service
  31 +@Slf4j
23 @HsqlDao 32 @HsqlDao
24 @Profile("install") 33 @Profile("install")
25 public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService 34 public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
@@ -27,5 +36,21 @@ public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSe @@ -27,5 +36,21 @@ public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSe
27 protected HsqlEntityDatabaseSchemaService() { 36 protected HsqlEntityDatabaseSchemaService() {
28 super("schema-entities-hsql.sql", "schema-entities-idx.sql"); 37 super("schema-entities-hsql.sql", "schema-entities-idx.sql");
29 } 38 }
  39 +
  40 + private final String schemaTypesSql = "schema-types-hsql.sql";
  41 +
  42 + @Override
  43 + public void createDatabaseSchema(boolean createIndexes) throws Exception {
  44 +
  45 + log.info("Installing SQL DataBase types part: " + schemaTypesSql);
  46 +
  47 + Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaTypesSql);
  48 + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
  49 + String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8"));
  50 + conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
  51 + }
  52 +
  53 + super.createDatabaseSchema(createIndexes);
  54 + }
30 } 55 }
31 56
@@ -30,7 +30,7 @@ import java.sql.SQLException; @@ -30,7 +30,7 @@ import java.sql.SQLException;
30 @Slf4j 30 @Slf4j
31 public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchemaService { 31 public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchemaService {
32 32
33 - private static final String SQL_DIR = "sql"; 33 + protected static final String SQL_DIR = "sql";
34 34
35 @Value("${spring.datasource.url}") 35 @Value("${spring.datasource.url}")
36 protected String dbUrl; 36 protected String dbUrl;
@@ -42,7 +42,7 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema @@ -42,7 +42,7 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
42 protected String dbPassword; 42 protected String dbPassword;
43 43
44 @Autowired 44 @Autowired
45 - private InstallScripts installScripts; 45 + protected InstallScripts installScripts;
46 46
47 private final String schemaSql; 47 private final String schemaSql;
48 private final String schemaIdxSql; 48 private final String schemaIdxSql;
@@ -59,6 +59,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; @@ -59,6 +59,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
59 import org.thingsboard.server.queue.util.TbCoreComponent; 59 import org.thingsboard.server.queue.util.TbCoreComponent;
60 import org.thingsboard.server.service.queue.TbClusterService; 60 import org.thingsboard.server.service.queue.TbClusterService;
61 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; 61 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
  62 +import org.thingsboard.server.utils.EventDeduplicationExecutor;
62 63
63 import javax.annotation.Nullable; 64 import javax.annotation.Nullable;
64 import javax.annotation.PostConstruct; 65 import javax.annotation.PostConstruct;
@@ -126,13 +127,13 @@ public class DefaultDeviceStateService implements DeviceStateService { @@ -126,13 +127,13 @@ public class DefaultDeviceStateService implements DeviceStateService {
126 @Getter 127 @Getter
127 private int initFetchPackSize; 128 private int initFetchPackSize;
128 129
129 - private volatile boolean clusterUpdatePending = false;  
130 -  
131 private ListeningScheduledExecutorService queueExecutor; 130 private ListeningScheduledExecutorService queueExecutor;
132 private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>(); 131 private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>();
133 private final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>(); 132 private final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
134 private final ConcurrentMap<DeviceId, Long> deviceLastReportedActivity = new ConcurrentHashMap<>(); 133 private final ConcurrentMap<DeviceId, Long> deviceLastReportedActivity = new ConcurrentHashMap<>();
135 private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>(); 134 private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>();
  135 + private volatile EventDeduplicationExecutor<Set<TopicPartitionInfo>> deduplicationExecutor;
  136 +
136 137
137 public DefaultDeviceStateService(TenantService tenantService, DeviceService deviceService, 138 public DefaultDeviceStateService(TenantService tenantService, DeviceService deviceService,
138 AttributesService attributesService, TimeseriesService tsService, 139 AttributesService attributesService, TimeseriesService tsService,
@@ -155,6 +156,7 @@ public class DefaultDeviceStateService implements DeviceStateService { @@ -155,6 +156,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
155 // Should be always single threaded due to absence of locks. 156 // Should be always single threaded due to absence of locks.
156 queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-state"))); 157 queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-state")));
157 queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS); 158 queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
  159 + deduplicationExecutor = new EventDeduplicationExecutor<>(DefaultDeviceStateService.class.getSimpleName(), queueExecutor, this::initStateFromDB);
158 } 160 }
159 161
160 @PreDestroy 162 @PreDestroy
@@ -292,25 +294,14 @@ public class DefaultDeviceStateService implements DeviceStateService { @@ -292,25 +294,14 @@ public class DefaultDeviceStateService implements DeviceStateService {
292 } 294 }
293 } 295 }
294 296
295 - volatile Set<TopicPartitionInfo> pendingPartitions;  
296 -  
297 @Override 297 @Override
298 public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { 298 public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
299 if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { 299 if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
300 - synchronized (this) {  
301 - pendingPartitions = partitionChangeEvent.getPartitions();  
302 - if (!clusterUpdatePending) {  
303 - clusterUpdatePending = true;  
304 - queueExecutor.submit(() -> {  
305 - clusterUpdatePending = false;  
306 - initStateFromDB();  
307 - });  
308 - }  
309 - } 300 + deduplicationExecutor.submit(partitionChangeEvent.getPartitions());
310 } 301 }
311 } 302 }
312 303
313 - private void initStateFromDB() { 304 + private void initStateFromDB(Set<TopicPartitionInfo> pendingPartitions) {
314 try { 305 try {
315 log.info("CURRENT PARTITIONS: {}", partitionedDevices.keySet()); 306 log.info("CURRENT PARTITIONS: {}", partitionedDevices.keySet());
316 log.info("NEW PARTITIONS: {}", pendingPartitions); 307 log.info("NEW PARTITIONS: {}", pendingPartitions);
@@ -302,7 +302,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -302,7 +302,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
302 Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); 302 Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
303 TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, 303 TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
304 attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription); 304 attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
305 - ctx.setAndResolveQuery(cmd.getQuery()); 305 + if (cmd.getQuery() != null) {
  306 + ctx.setAndResolveQuery(cmd.getQuery());
  307 + }
306 sessionSubs.put(cmd.getCmdId(), ctx); 308 sessionSubs.put(cmd.getCmdId(), ctx);
307 return ctx; 309 return ctx;
308 } 310 }
@@ -107,7 +107,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends @@ -107,7 +107,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
107 public void setAndResolveQuery(T query) { 107 public void setAndResolveQuery(T query) {
108 dynamicValues.clear(); 108 dynamicValues.clear();
109 this.query = query; 109 this.query = query;
110 - if (query.getKeyFilters() != null) { 110 + if (query != null && query.getKeyFilters() != null) {
111 for (KeyFilter filter : query.getKeyFilters()) { 111 for (KeyFilter filter : query.getKeyFilters()) {
112 registerDynamicValues(filter.getPredicate()); 112 registerDynamicValues(filter.getPredicate());
113 } 113 }
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.utils;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +
  20 +import java.util.concurrent.Executor;
  21 +import java.util.concurrent.ExecutorService;
  22 +import java.util.function.Consumer;
  23 +
  24 +/**
  25 + * This class deduplicate executions of the specified function.
  26 + * Useful in cluster mode, when you get event about partition change multiple times.
  27 + * Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and
  28 + * later, once the processing of first event is done, process last pending task.
  29 + *
  30 + * @param <P> parameters of the function
  31 + */
  32 +@Slf4j
  33 +public class EventDeduplicationExecutor<P> {
  34 + private final String name;
  35 + private final ExecutorService executor;
  36 + private final Consumer<P> function;
  37 + private P pendingTask;
  38 + private boolean busy;
  39 +
  40 + public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer<P> function) {
  41 + this.name = name;
  42 + this.executor = executor;
  43 + this.function = function;
  44 + }
  45 +
  46 + public void submit(P params) {
  47 + log.info("[{}] Going to submit: {}", name, params);
  48 + synchronized (EventDeduplicationExecutor.this) {
  49 + if (!busy) {
  50 + busy = true;
  51 + pendingTask = null;
  52 + try {
  53 + log.info("[{}] Submitting task: {}", name, params);
  54 + executor.submit(() -> {
  55 + try {
  56 + log.info("[{}] Executing task: {}", name, params);
  57 + function.accept(params);
  58 + } catch (Throwable e) {
  59 + log.warn("[{}] Failed to process task with parameters: {}", name, params, e);
  60 + throw e;
  61 + } finally {
  62 + unlockAndProcessIfAny();
  63 + }
  64 + });
  65 + } catch (Throwable e) {
  66 + log.warn("[{}] Failed to submit task with parameters: {}", name, params, e);
  67 + unlockAndProcessIfAny();
  68 + throw e;
  69 + }
  70 + } else {
  71 + log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params);
  72 + pendingTask = params;
  73 + }
  74 + }
  75 + }
  76 +
  77 + private void unlockAndProcessIfAny() {
  78 + synchronized (EventDeduplicationExecutor.this) {
  79 + busy = false;
  80 + if (pendingTask != null) {
  81 + submit(pendingTask);
  82 + }
  83 + }
  84 + }
  85 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.util;
  17 +
  18 +import com.google.common.util.concurrent.MoreExecutors;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.junit.Test;
  21 +import org.junit.runner.RunWith;
  22 +import org.mockito.Mockito;
  23 +import org.mockito.runners.MockitoJUnitRunner;
  24 +import org.thingsboard.server.utils.EventDeduplicationExecutor;
  25 +
  26 +import java.util.concurrent.ExecutorService;
  27 +import java.util.concurrent.Executors;
  28 +import java.util.function.Consumer;
  29 +
  30 +@Slf4j
  31 +@RunWith(MockitoJUnitRunner.class)
  32 +public class EventDeduplicationExecutorTest {
  33 +
  34 + @Test
  35 + public void testSimpleFlowSameThread() throws InterruptedException {
  36 + simpleFlow(MoreExecutors.newDirectExecutorService());
  37 + }
  38 +
  39 + @Test
  40 + public void testPeriodicFlowSameThread() throws InterruptedException {
  41 + periodicFlow(MoreExecutors.newDirectExecutorService());
  42 + }
  43 +
  44 + @Test
  45 + public void testExceptionFlowSameThread() throws InterruptedException {
  46 + exceptionFlow(MoreExecutors.newDirectExecutorService());
  47 + }
  48 +
  49 + @Test
  50 + public void testSimpleFlowSingleThread() throws InterruptedException {
  51 + simpleFlow(Executors.newSingleThreadExecutor());
  52 + }
  53 +
  54 + @Test
  55 + public void testPeriodicFlowSingleThread() throws InterruptedException {
  56 + periodicFlow(Executors.newSingleThreadExecutor());
  57 + }
  58 +
  59 + @Test
  60 + public void testExceptionFlowSingleThread() throws InterruptedException {
  61 + exceptionFlow(Executors.newSingleThreadExecutor());
  62 + }
  63 +
  64 + @Test
  65 + public void testSimpleFlowMultiThread() throws InterruptedException {
  66 + simpleFlow(Executors.newFixedThreadPool(3));
  67 + }
  68 +
  69 + @Test
  70 + public void testPeriodicFlowMultiThread() throws InterruptedException {
  71 + periodicFlow(Executors.newFixedThreadPool(3));
  72 + }
  73 +
  74 + @Test
  75 + public void testExceptionFlowMultiThread() throws InterruptedException {
  76 + exceptionFlow(Executors.newFixedThreadPool(3));
  77 + }
  78 +
  79 + private void simpleFlow(ExecutorService executorService) throws InterruptedException {
  80 + try {
  81 + Consumer<String> function = Mockito.spy(StringConsumer.class);
  82 + EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
  83 +
  84 + String params1 = "params1";
  85 + String params2 = "params2";
  86 + String params3 = "params3";
  87 +
  88 + executor.submit(params1);
  89 + executor.submit(params2);
  90 + executor.submit(params3);
  91 + Thread.sleep(500);
  92 + Mockito.verify(function).accept(params1);
  93 + Mockito.verify(function).accept(params3);
  94 + } finally {
  95 + executorService.shutdownNow();
  96 + }
  97 + }
  98 +
  99 + private void periodicFlow(ExecutorService executorService) throws InterruptedException {
  100 + try {
  101 + Consumer<String> function = Mockito.spy(StringConsumer.class);
  102 + EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
  103 +
  104 + String params1 = "params1";
  105 + String params2 = "params2";
  106 + String params3 = "params3";
  107 +
  108 + executor.submit(params1);
  109 + Thread.sleep(500);
  110 + executor.submit(params2);
  111 + Thread.sleep(500);
  112 + executor.submit(params3);
  113 + Thread.sleep(500);
  114 + Mockito.verify(function).accept(params1);
  115 + Mockito.verify(function).accept(params2);
  116 + Mockito.verify(function).accept(params3);
  117 + } finally {
  118 + executorService.shutdownNow();
  119 + }
  120 + }
  121 +
  122 + private void exceptionFlow(ExecutorService executorService) throws InterruptedException {
  123 + try {
  124 + Consumer<String> function = Mockito.spy(StringConsumer.class);
  125 + EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
  126 +
  127 + String params1 = "params1";
  128 + String params2 = "params2";
  129 + String params3 = "params3";
  130 +
  131 + Mockito.doThrow(new RuntimeException()).when(function).accept("params1");
  132 + executor.submit(params1);
  133 + executor.submit(params2);
  134 + Thread.sleep(500);
  135 + executor.submit(params3);
  136 + Thread.sleep(500);
  137 + Mockito.verify(function).accept(params2);
  138 + Mockito.verify(function).accept(params3);
  139 + } finally {
  140 + executorService.shutdownNow();
  141 + }
  142 + }
  143 +
  144 + public static class StringConsumer implements Consumer<String> {
  145 + @Override
  146 + public void accept(String s) {
  147 + try {
  148 + Thread.sleep(100);
  149 + } catch (InterruptedException e) {
  150 + throw new RuntimeException(e);
  151 + }
  152 + }
  153 + }
  154 +
  155 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class HomeDashboard extends Dashboard {
  22 +
  23 + private boolean hideDashboardToolbar;
  24 +
  25 + public HomeDashboard(Dashboard dashboard, boolean hideDashboardToolbar) {
  26 + super(dashboard);
  27 + this.hideDashboardToolbar = hideDashboardToolbar;
  28 + }
  29 +
  30 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data;
  17 +
  18 +import lombok.AllArgsConstructor;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.data.id.DashboardId;
  21 +
  22 +@Data
  23 +@AllArgsConstructor
  24 +public class HomeDashboardInfo {
  25 + private DashboardId dashboardId;
  26 + private boolean hideDashboardToolbar;
  27 +}
@@ -19,16 +19,18 @@ import com.google.common.util.concurrent.ListenableFuture; @@ -19,16 +19,18 @@ import com.google.common.util.concurrent.ListenableFuture;
19 import org.thingsboard.server.common.data.alarm.Alarm; 19 import org.thingsboard.server.common.data.alarm.Alarm;
20 import org.thingsboard.server.common.data.alarm.AlarmInfo; 20 import org.thingsboard.server.common.data.alarm.AlarmInfo;
21 import org.thingsboard.server.common.data.alarm.AlarmQuery; 21 import org.thingsboard.server.common.data.alarm.AlarmQuery;
  22 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
  23 +import org.thingsboard.server.common.data.alarm.AlarmStatus;
22 import org.thingsboard.server.common.data.id.CustomerId; 24 import org.thingsboard.server.common.data.id.CustomerId;
23 import org.thingsboard.server.common.data.id.EntityId; 25 import org.thingsboard.server.common.data.id.EntityId;
24 import org.thingsboard.server.common.data.id.TenantId; 26 import org.thingsboard.server.common.data.id.TenantId;
25 import org.thingsboard.server.common.data.page.PageData; 27 import org.thingsboard.server.common.data.page.PageData;
26 import org.thingsboard.server.common.data.query.AlarmData; 28 import org.thingsboard.server.common.data.query.AlarmData;
27 -import org.thingsboard.server.common.data.query.AlarmDataPageLink;  
28 import org.thingsboard.server.common.data.query.AlarmDataQuery; 29 import org.thingsboard.server.common.data.query.AlarmDataQuery;
29 import org.thingsboard.server.dao.Dao; 30 import org.thingsboard.server.dao.Dao;
30 31
31 import java.util.Collection; 32 import java.util.Collection;
  33 +import java.util.Set;
32 import java.util.UUID; 34 import java.util.UUID;
33 35
34 /** 36 /**
@@ -48,4 +50,6 @@ public interface AlarmDao extends Dao<Alarm> { @@ -48,4 +50,6 @@ public interface AlarmDao extends Dao<Alarm> {
48 50
49 PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, 51 PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId,
50 AlarmDataQuery query, Collection<EntityId> orderedEntityIds); 52 AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
  53 +
  54 + Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status);
51 } 55 }
@@ -39,10 +39,10 @@ import org.thingsboard.server.common.data.id.CustomerId; @@ -39,10 +39,10 @@ import org.thingsboard.server.common.data.id.CustomerId;
39 import org.thingsboard.server.common.data.id.EntityId; 39 import org.thingsboard.server.common.data.id.EntityId;
40 import org.thingsboard.server.common.data.id.TenantId; 40 import org.thingsboard.server.common.data.id.TenantId;
41 import org.thingsboard.server.common.data.page.PageData; 41 import org.thingsboard.server.common.data.page.PageData;
42 -import org.thingsboard.server.common.data.page.TimePageLink;  
43 import org.thingsboard.server.common.data.query.AlarmData; 42 import org.thingsboard.server.common.data.query.AlarmData;
44 import org.thingsboard.server.common.data.query.AlarmDataPageLink; 43 import org.thingsboard.server.common.data.query.AlarmDataPageLink;
45 import org.thingsboard.server.common.data.query.AlarmDataQuery; 44 import org.thingsboard.server.common.data.query.AlarmDataQuery;
  45 +import org.thingsboard.server.common.data.query.DeviceTypeFilter;
46 import org.thingsboard.server.common.data.relation.EntityRelation; 46 import org.thingsboard.server.common.data.relation.EntityRelation;
47 import org.thingsboard.server.common.data.relation.EntityRelationsQuery; 47 import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
48 import org.thingsboard.server.common.data.relation.EntitySearchDirection; 48 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@@ -60,7 +60,6 @@ import javax.annotation.PreDestroy; @@ -60,7 +60,6 @@ import javax.annotation.PreDestroy;
60 import java.util.ArrayList; 60 import java.util.ArrayList;
61 import java.util.Collection; 61 import java.util.Collection;
62 import java.util.Collections; 62 import java.util.Collections;
63 -import java.util.Comparator;  
64 import java.util.LinkedHashSet; 63 import java.util.LinkedHashSet;
65 import java.util.List; 64 import java.util.List;
66 import java.util.Set; 65 import java.util.Set;
@@ -316,37 +315,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @@ -316,37 +315,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
316 @Override 315 @Override
317 public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, 316 public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
318 AlarmStatus alarmStatus) { 317 AlarmStatus alarmStatus) {
319 - TimePageLink nextPageLink = new TimePageLink(100);  
320 - boolean hasNext = true;  
321 - AlarmSeverity highestSeverity = null;  
322 - AlarmQuery query;  
323 - while (hasNext && AlarmSeverity.CRITICAL != highestSeverity) {  
324 - query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false, null);  
325 - PageData<AlarmInfo> alarms = alarmDao.findAlarms(tenantId, query);  
326 - if (alarms.hasNext()) {  
327 - nextPageLink = nextPageLink.nextPageLink();  
328 - }  
329 - AlarmSeverity severity = detectHighestSeverity(alarms.getData());  
330 - if (severity == null) {  
331 - continue;  
332 - }  
333 - if (severity == AlarmSeverity.CRITICAL || highestSeverity == null) {  
334 - highestSeverity = severity;  
335 - } else {  
336 - highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity;  
337 - } 318 + Set<AlarmStatus> statusList = null;
  319 + if (alarmSearchStatus != null) {
  320 + statusList = alarmSearchStatus.getStatuses();
  321 + } else if (alarmStatus != null) {
  322 + statusList = Collections.singleton(alarmStatus);
338 } 323 }
339 - return highestSeverity;  
340 - }  
341 324
342 - private AlarmSeverity detectHighestSeverity(List<AlarmInfo> alarms) {  
343 - if (!alarms.isEmpty()) {  
344 - List<AlarmInfo> sorted = new ArrayList(alarms);  
345 - sorted.sort(Comparator.comparing(Alarm::getSeverity));  
346 - return sorted.get(0).getSeverity();  
347 - } else {  
348 - return null;  
349 - } 325 + Set<AlarmSeverity> alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList);
  326 +
  327 + return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null);
350 } 328 }
351 329
352 private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) { 330 private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) {
@@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable;
20 import org.springframework.data.jpa.repository.Query; 20 import org.springframework.data.jpa.repository.Query;
21 import org.springframework.data.repository.CrudRepository; 21 import org.springframework.data.repository.CrudRepository;
22 import org.springframework.data.repository.query.Param; 22 import org.springframework.data.repository.query.Param;
  23 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
23 import org.thingsboard.server.common.data.alarm.AlarmStatus; 24 import org.thingsboard.server.common.data.alarm.AlarmStatus;
24 import org.thingsboard.server.dao.model.sql.AlarmEntity; 25 import org.thingsboard.server.dao.model.sql.AlarmEntity;
25 import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; 26 import org.thingsboard.server.dao.model.sql.AlarmInfoEntity;
@@ -75,4 +76,12 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> { @@ -75,4 +76,12 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
75 @Param("searchText") String searchText, 76 @Param("searchText") String searchText,
76 Pageable pageable); 77 Pageable pageable);
77 78
  79 + @Query("SELECT alarm.severity FROM AlarmEntity alarm" +
  80 + " WHERE alarm.tenantId = :tenantId" +
  81 + " AND alarm.originatorId = :entityId" +
  82 + " AND ((:status) IS NULL OR alarm.status in (:status))")
  83 + Set<AlarmSeverity> findAlarmSeverities(@Param("tenantId") UUID tenantId,
  84 + @Param("entityId") UUID entityId,
  85 + @Param("status") Set<AlarmStatus> status);
  86 +
78 } 87 }
@@ -24,6 +24,7 @@ import org.springframework.stereotype.Component; @@ -24,6 +24,7 @@ import org.springframework.stereotype.Component;
24 import org.thingsboard.server.common.data.alarm.Alarm; 24 import org.thingsboard.server.common.data.alarm.Alarm;
25 import org.thingsboard.server.common.data.alarm.AlarmInfo; 25 import org.thingsboard.server.common.data.alarm.AlarmInfo;
26 import org.thingsboard.server.common.data.alarm.AlarmQuery; 26 import org.thingsboard.server.common.data.alarm.AlarmQuery;
  27 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
27 import org.thingsboard.server.common.data.alarm.AlarmStatus; 28 import org.thingsboard.server.common.data.alarm.AlarmStatus;
28 import org.thingsboard.server.common.data.id.CustomerId; 29 import org.thingsboard.server.common.data.id.CustomerId;
29 import org.thingsboard.server.common.data.id.EntityId; 30 import org.thingsboard.server.common.data.id.EntityId;
@@ -120,4 +121,9 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A @@ -120,4 +121,9 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
120 public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) { 121 public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
121 return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds); 122 return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds);
122 } 123 }
  124 +
  125 + @Override
  126 + public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status) {
  127 + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status);
  128 + }
123 } 129 }
@@ -83,7 +83,7 @@ public class EntityDataAdapter { @@ -83,7 +83,7 @@ public class EntityDataAdapter {
83 if (value != null) { 83 if (value != null) {
84 String strVal = value.toString(); 84 String strVal = value.toString();
85 // check number 85 // check number
86 - if (strVal.length() > 0 && NumberUtils.isParsable(strVal)) { 86 + if (NumberUtils.isNumber(strVal)) {
87 try { 87 try {
88 long longVal = Long.parseLong(strVal); 88 long longVal = Long.parseLong(strVal);
89 return Long.toString(longVal); 89 return Long.toString(longVal);
@@ -116,16 +116,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -116,16 +116,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
116 super.startExecutor(); 116 super.startExecutor();
117 if (!isInstall()) { 117 if (!isInstall()) {
118 getFetchStmt(Aggregation.NONE, DESC_ORDER); 118 getFetchStmt(Aggregation.NONE, DESC_ORDER);
119 - Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);  
120 - if (partition.isPresent()) {  
121 - tsFormat = partition.get();  
122 - if (!isFixedPartitioning() && partitionsCacheSize > 0) {  
123 - cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize);  
124 - }  
125 - } else {  
126 - log.warn("Incorrect configuration of partitioning {}", partitioning);  
127 - throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); 119 + }
  120 + Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);
  121 + if (partition.isPresent()) {
  122 + tsFormat = partition.get();
  123 + if (!isFixedPartitioning() && partitionsCacheSize > 0) {
  124 + cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize);
128 } 125 }
  126 + } else {
  127 + log.warn("Incorrect configuration of partitioning {}", partitioning);
  128 + throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
129 } 129 }
130 } 130 }
131 131
@@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS customer ( @@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS customer (
114 CREATE TABLE IF NOT EXISTS dashboard ( 114 CREATE TABLE IF NOT EXISTS dashboard (
115 id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, 115 id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
116 created_time bigint NOT NULL, 116 created_time bigint NOT NULL,
117 - configuration varchar, 117 + configuration varchar(10000000),
118 assigned_customers varchar(1000000), 118 assigned_customers varchar(1000000),
119 search_text varchar(255), 119 search_text varchar(255),
120 tenant_id uuid, 120 tenant_id uuid,
@@ -355,6 +355,62 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { @@ -355,6 +355,62 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
355 } 355 }
356 356
357 @Test 357 @Test
  358 + public void testFindHighestAlarmSeverity() throws ExecutionException, InterruptedException {
  359 + Customer customer = new Customer();
  360 + customer.setTitle("TestCustomer");
  361 + customer.setTenantId(tenantId);
  362 + customer = customerService.saveCustomer(customer);
  363 +
  364 + Device customerDevice = new Device();
  365 + customerDevice.setName("TestCustomerDevice");
  366 + customerDevice.setType("default");
  367 + customerDevice.setTenantId(tenantId);
  368 + customerDevice.setCustomerId(customer.getId());
  369 + customerDevice = deviceService.saveDevice(customerDevice);
  370 +
  371 + // no one alarms was created
  372 + Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null));
  373 +
  374 + Alarm alarm1 = Alarm.builder()
  375 + .tenantId(tenantId)
  376 + .originator(customerDevice.getId())
  377 + .type(TEST_ALARM)
  378 + .severity(AlarmSeverity.MAJOR)
  379 + .status(AlarmStatus.ACTIVE_UNACK)
  380 + .startTs(System.currentTimeMillis())
  381 + .build();
  382 + alarm1 = alarmService.createOrUpdateAlarm(alarm1).getAlarm();
  383 + alarmService.clearAlarm(tenantId, alarm1.getId(), null, System.currentTimeMillis()).get();
  384 +
  385 + Alarm alarm2 = Alarm.builder()
  386 + .tenantId(tenantId)
  387 + .originator(customerDevice.getId())
  388 + .type(TEST_ALARM)
  389 + .severity(AlarmSeverity.MINOR)
  390 + .status(AlarmStatus.ACTIVE_ACK)
  391 + .startTs(System.currentTimeMillis())
  392 + .build();
  393 + alarm2 = alarmService.createOrUpdateAlarm(alarm2).getAlarm();
  394 + alarmService.clearAlarm(tenantId, alarm2.getId(), null, System.currentTimeMillis()).get();
  395 +
  396 + Alarm alarm3 = Alarm.builder()
  397 + .tenantId(tenantId)
  398 + .originator(customerDevice.getId())
  399 + .type(TEST_ALARM)
  400 + .severity(AlarmSeverity.CRITICAL)
  401 + .status(AlarmStatus.ACTIVE_ACK)
  402 + .startTs(System.currentTimeMillis())
  403 + .build();
  404 + alarm3 = alarmService.createOrUpdateAlarm(alarm3).getAlarm();
  405 +
  406 + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null));
  407 + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null));
  408 + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK));
  409 + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null));
  410 + Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK));
  411 + }
  412 +
  413 + @Test
358 public void testFindAlarmUsingAlarmDataQuery() throws ExecutionException, InterruptedException { 414 public void testFindAlarmUsingAlarmDataQuery() throws ExecutionException, InterruptedException {
359 AssetId parentId = new AssetId(Uuids.timeBased()); 415 AssetId parentId = new AssetId(Uuids.timeBased());
360 AssetId parentId2 = new AssetId(Uuids.timeBased()); 416 AssetId parentId2 = new AssetId(Uuids.timeBased());
@@ -827,10 +827,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { @@ -827,10 +827,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
827 .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); 827 .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
828 } 828 }
829 List<String> deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); 829 List<String> deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
830 - if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) {  
831 - // in H2 double values are stored with E0 in the end of the string  
832 - loadedTemperatures = loadedTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList());  
833 - } 830 +
834 Assert.assertEquals(deviceTemperatures, loadedTemperatures); 831 Assert.assertEquals(deviceTemperatures, loadedTemperatures);
835 832
836 pageLink = new EntityDataPageLink(10, 0, null, sortOrder); 833 pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
@@ -858,10 +855,6 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { @@ -858,10 +855,6 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
858 entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList()); 855 entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList());
859 List<String> deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); 856 List<String> deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
860 857
861 - if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) {  
862 - // in H2 double values are stored with E0 in the end of the string  
863 - loadedHighTemperatures = loadedHighTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList());  
864 - }  
865 Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); 858 Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
866 859
867 deviceService.deleteDevicesByTenantId(tenantId); 860 deviceService.deleteDevicesByTenantId(tenantId);
@@ -18,7 +18,7 @@ FROM thingsboard/openjdk8 @@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
18 18
19 RUN apt-get update 19 RUN apt-get update
20 RUN apt-get install -y curl nmap procps 20 RUN apt-get install -y curl nmap procps
21 -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null 21 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
23 RUN echo 'deb http://www.apache.org/dist/cassandra/debian 311x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null 23 RUN echo 'deb http://www.apache.org/dist/cassandra/debian 311x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
24 RUN curl -L https://www.apache.org/dist/cassandra/KEYS | apt-key add - 24 RUN curl -L https://www.apache.org/dist/cassandra/KEYS | apt-key add -
@@ -18,7 +18,7 @@ FROM thingsboard/openjdk8 @@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
18 18
19 RUN apt-get update 19 RUN apt-get update
20 RUN apt-get install -y curl 20 RUN apt-get install -y curl
21 -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null 21 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
23 ENV PG_MAJOR 11 23 ENV PG_MAJOR 11
24 RUN apt-get update 24 RUN apt-get update
@@ -1189,7 +1189,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -1189,7 +1189,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1189 1189
1190 public List<EntitySubtype> getDeviceTypes() { 1190 public List<EntitySubtype> getDeviceTypes() {
1191 return restTemplate.exchange( 1191 return restTemplate.exchange(
1192 - baseURL + "/api/devices", 1192 + baseURL + "/api/device/types",
1193 HttpMethod.GET, 1193 HttpMethod.GET,
1194 HttpEntity.EMPTY, 1194 HttpEntity.EMPTY,
1195 new ParameterizedTypeReference<List<EntitySubtype>>() { 1195 new ParameterizedTypeReference<List<EntitySubtype>>() {
@@ -61,4 +61,12 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA @@ -61,4 +61,12 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
61 configuration.setCredentials(new AnonymousCredentials()); 61 configuration.setCredentials(new AnonymousCredentials());
62 return configuration; 62 return configuration;
63 } 63 }
  64 +
  65 + public ClientCredentials getCredentials() {
  66 + if (this.credentials == null) {
  67 + return new AnonymousCredentials();
  68 + } else {
  69 + return this.credentials;
  70 + }
  71 + }
64 } 72 }
@@ -53,8 +53,8 @@ export class AliasController implements IAliasController { @@ -53,8 +53,8 @@ export class AliasController implements IAliasController {
53 private stateControllerHolder: StateControllerHolder, 53 private stateControllerHolder: StateControllerHolder,
54 private origEntityAliases: EntityAliases, 54 private origEntityAliases: EntityAliases,
55 private origFilters: Filters) { 55 private origFilters: Filters) {
56 - this.entityAliases = deepClone(this.origEntityAliases);  
57 - this.filters = deepClone(this.origFilters); 56 + this.entityAliases = deepClone(this.origEntityAliases) || {};
  57 + this.filters = deepClone(this.origFilters) || {};
58 this.userFilters = {}; 58 this.userFilters = {};
59 } 59 }
60 60
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 /// 15 ///
16 16
17 import { Injectable, NgZone } from '@angular/core'; 17 import { Injectable, NgZone } from '@angular/core';
18 -import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'; 18 +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
19 import { AuthService } from '../auth/auth.service'; 19 import { AuthService } from '../auth/auth.service';
20 import { select, Store } from '@ngrx/store'; 20 import { select, Store } from '@ngrx/store';
21 import { AppState } from '../core.state'; 21 import { AppState } from '../core.state';
@@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum'; @@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum';
28 import { DialogService } from '@core/services/dialog.service'; 28 import { DialogService } from '@core/services/dialog.service';
29 import { TranslateService } from '@ngx-translate/core'; 29 import { TranslateService } from '@ngx-translate/core';
30 import { UtilsService } from '@core/services/utils.service'; 30 import { UtilsService } from '@core/services/utils.service';
  31 +import { isObject } from '@core/utils';
31 32
32 @Injectable({ 33 @Injectable({
33 providedIn: 'root' 34 providedIn: 'root'
@@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service'; @@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
35 export class AuthGuard implements CanActivate, CanActivateChild { 36 export class AuthGuard implements CanActivate, CanActivateChild {
36 37
37 constructor(private store: Store<AppState>, 38 constructor(private store: Store<AppState>,
  39 + private router: Router,
38 private authService: AuthService, 40 private authService: AuthService,
39 private dialogService: DialogService, 41 private dialogService: DialogService,
40 private utils: UtilsService, 42 private utils: UtilsService,
@@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild {
115 if (data.auth && data.auth.indexOf(authority) === -1) { 117 if (data.auth && data.auth.indexOf(authority) === -1) {
116 this.dialogService.forbidden(); 118 this.dialogService.forbidden();
117 return of(false); 119 return of(false);
  120 + } else if (data.redirectTo) {
  121 + let redirect;
  122 + if (isObject(data.redirectTo)) {
  123 + redirect = data.redirectTo[authority];
  124 + } else {
  125 + redirect = data.redirectTo;
  126 + }
  127 + return of(this.router.parseUrl(redirect));
118 } else { 128 } else {
119 return of(true); 129 return of(true);
120 } 130 }
@@ -20,7 +20,7 @@ import { Observable } from 'rxjs'; @@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
20 import { HttpClient } from '@angular/common/http'; 20 import { HttpClient } from '@angular/common/http';
21 import { PageLink } from '@shared/models/page/page-link'; 21 import { PageLink } from '@shared/models/page/page-link';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
23 -import { Dashboard, DashboardInfo } from '@shared/models/dashboard.models'; 23 +import { Dashboard, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models';
24 import { WINDOW } from '@core/services/window.service'; 24 import { WINDOW } from '@core/services/window.service';
25 import { NavigationEnd, Router } from '@angular/router'; 25 import { NavigationEnd, Router } from '@angular/router';
26 import { filter, map, publishReplay, refCount } from 'rxjs/operators'; 26 import { filter, map, publishReplay, refCount } from 'rxjs/operators';
@@ -122,6 +122,19 @@ export class DashboardService { @@ -122,6 +122,19 @@ export class DashboardService {
122 defaultHttpOptionsFromConfig(config)); 122 defaultHttpOptionsFromConfig(config));
123 } 123 }
124 124
  125 + public getHomeDashboard(config?: RequestConfig): Observable<HomeDashboard> {
  126 + return this.http.get<HomeDashboard>('/api/dashboard/home', defaultHttpOptionsFromConfig(config));
  127 + }
  128 +
  129 + public getTenantHomeDashboardInfo(config?: RequestConfig): Observable<HomeDashboardInfo> {
  130 + return this.http.get<HomeDashboardInfo>('/api/tenant/dashboard/home/info', defaultHttpOptionsFromConfig(config));
  131 + }
  132 +
  133 + public setTenantHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo, config?: RequestConfig): Observable<any> {
  134 + return this.http.post<any>('/api/tenant/dashboard/home/info', homeDashboardInfo,
  135 + defaultHttpOptionsFromConfig(config));
  136 + }
  137 +
125 public getPublicDashboardLink(dashboard: DashboardInfo): string | null { 138 public getPublicDashboardLink(dashboard: DashboardInfo): string | null {
126 if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) { 139 if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) {
127 const publicCustomers = dashboard.assignedCustomers 140 const publicCustomers = dashboard.assignedCustomers
@@ -223,6 +223,7 @@ export class MenuService { @@ -223,6 +223,7 @@ export class MenuService {
223 name: 'home.home', 223 name: 'home.home',
224 type: 'link', 224 type: 'link',
225 path: '/home', 225 path: '/home',
  226 + notExact: true,
226 icon: 'home' 227 icon: 'home'
227 }, 228 },
228 { 229 {
@@ -284,6 +285,13 @@ export class MenuService { @@ -284,6 +285,13 @@ export class MenuService {
284 }, 285 },
285 { 286 {
286 id: guid(), 287 id: guid(),
  288 + name: 'admin.home-settings',
  289 + type: 'link',
  290 + path: '/settings/home',
  291 + icon: 'settings_applications'
  292 + },
  293 + {
  294 + id: guid(),
287 name: 'audit-log.audit-logs', 295 name: 'audit-log.audit-logs',
288 type: 'link', 296 type: 'link',
289 path: '/auditLogs', 297 path: '/auditLogs',
@@ -402,6 +410,7 @@ export class MenuService { @@ -402,6 +410,7 @@ export class MenuService {
402 name: 'home.home', 410 name: 'home.home',
403 type: 'link', 411 type: 'link',
404 path: '/home', 412 path: '/home',
  413 + notExact: true,
405 icon: 'home' 414 icon: 'home'
406 }, 415 },
407 { 416 {
@@ -14,14 +14,23 @@ @@ -14,14 +14,23 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 17 +import {
  18 + Component,
  19 + Injector,
  20 + Input,
  21 + OnDestroy,
  22 + OnInit,
  23 + StaticProvider,
  24 + ViewChild,
  25 + ViewContainerRef
  26 +} from '@angular/core';
18 import { TooltipPosition } from '@angular/material/tooltip'; 27 import { TooltipPosition } from '@angular/material/tooltip';
19 import { AliasInfo, IAliasController } from '@core/api/widget-api.models'; 28 import { AliasInfo, IAliasController } from '@core/api/widget-api.models';
20 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 29 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
21 import { TranslateService } from '@ngx-translate/core'; 30 import { TranslateService } from '@ngx-translate/core';
22 import { Subscription } from 'rxjs'; 31 import { Subscription } from 'rxjs';
23 import { BreakpointObserver } from '@angular/cdk/layout'; 32 import { BreakpointObserver } from '@angular/cdk/layout';
24 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 33 +import { ComponentPortal } from '@angular/cdk/portal';
25 import { 34 import {
26 ALIASES_ENTITY_SELECT_PANEL_DATA, 35 ALIASES_ENTITY_SELECT_PANEL_DATA,
27 AliasesEntitySelectPanelComponent, 36 AliasesEntitySelectPanelComponent,
@@ -136,12 +145,12 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { @@ -136,12 +145,12 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy {
136 overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector)); 145 overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector));
137 } 146 }
138 147
139 - private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): PortalInjector {  
140 - const injectionTokens = new WeakMap<any, any>([  
141 - [ALIASES_ENTITY_SELECT_PANEL_DATA, data],  
142 - [OverlayRef, overlayRef]  
143 - ]);  
144 - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); 148 + private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): Injector {
  149 + const providers: StaticProvider[] = [
  150 + {provide: ALIASES_ENTITY_SELECT_PANEL_DATA, useValue: data},
  151 + {provide: OverlayRef, useValue: overlayRef}
  152 + ];
  153 + return Injector.create({parent: this.viewContainerRef.injector, providers});
145 } 154 }
146 155
147 private updateDisplayValue() { 156 private updateDisplayValue() {
@@ -195,7 +195,8 @@ @@ -195,7 +195,8 @@
195 [length]="dataSource.total() | async" 195 [length]="dataSource.total() | async"
196 [pageIndex]="pageLink.page" 196 [pageIndex]="pageLink.page"
197 [pageSize]="pageLink.pageSize" 197 [pageSize]="pageLink.pageSize"
198 - [pageSizeOptions]="[10, 20, 30]"></mat-paginator> 198 + [pageSizeOptions]="[10, 20, 30]"
  199 + showFirstLastButtons></mat-paginator>
199 <ngx-hm-carousel fxFlex *ngIf="mode === 'widget' && widgetsList.length > 0" 200 <ngx-hm-carousel fxFlex *ngIf="mode === 'widget' && widgetsList.length > 0"
200 #carousel 201 #carousel
201 [(ngModel)]="widgetsCarouselIndex" 202 [(ngModel)]="widgetsCarouselIndex"
@@ -19,9 +19,11 @@ import { @@ -19,9 +19,11 @@ import {
19 ChangeDetectionStrategy, 19 ChangeDetectionStrategy,
20 Component, 20 Component,
21 ElementRef, 21 ElementRef,
  22 + Injector,
22 Input, 23 Input,
23 NgZone, 24 NgZone,
24 OnInit, 25 OnInit,
  26 + StaticProvider,
25 ViewChild, 27 ViewChild,
26 ViewContainerRef 28 ViewContainerRef
27 } from '@angular/core'; 29 } from '@angular/core';
@@ -62,7 +64,7 @@ import { @@ -62,7 +64,7 @@ import {
62 EditAttributeValuePanelComponent, 64 EditAttributeValuePanelComponent,
63 EditAttributeValuePanelData 65 EditAttributeValuePanelData
64 } from './edit-attribute-value-panel.component'; 66 } from './edit-attribute-value-panel.component';
65 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 67 +import { ComponentPortal } from '@angular/cdk/portal';
66 import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; 68 import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
67 import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 69 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
68 import { DataKey, Datasource, DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; 70 import { DataKey, Datasource, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
@@ -319,13 +321,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI @@ -319,13 +321,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
319 overlayRef.backdropClick().subscribe(() => { 321 overlayRef.backdropClick().subscribe(() => {
320 overlayRef.dispose(); 322 overlayRef.dispose();
321 }); 323 });
322 - const injectionTokens = new WeakMap<any, any>([  
323 - [EDIT_ATTRIBUTE_VALUE_PANEL_DATA, {  
324 - attributeValue: attribute.value  
325 - } as EditAttributeValuePanelData],  
326 - [OverlayRef, overlayRef]  
327 - ]);  
328 - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); 324 + const providers: StaticProvider[] = [
  325 + {
  326 + provide: EDIT_ATTRIBUTE_VALUE_PANEL_DATA,
  327 + useValue: {
  328 + attributeValue: attribute.value
  329 + } as EditAttributeValuePanelData
  330 + },
  331 + {
  332 + provide: OverlayRef,
  333 + useValue: overlayRef
  334 + }
  335 + ];
  336 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
329 const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent, 337 const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent,
330 this.viewContainerRef, injector)); 338 this.viewContainerRef, injector));
331 componentRef.onDestroy(() => { 339 componentRef.onDestroy(() => {
@@ -49,10 +49,11 @@ @@ -49,10 +49,11 @@
49 <tb-states-component fxFlex.lt-md 49 <tb-states-component fxFlex.lt-md
50 [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId" 50 [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
51 [dashboardCtrl]="this" 51 [dashboardCtrl]="this"
52 - [dashboardId]="dashboard.id ? dashboard.id.id : ''" 52 + [dashboardId]="(!embedded && dashboard.id) ? dashboard.id.id : ''"
53 [isMobile]="isMobile" 53 [isMobile]="isMobile"
54 [state]="dashboardCtx.state" 54 [state]="dashboardCtx.state"
55 [currentState]="currentState" 55 [currentState]="currentState"
  56 + [syncStateWithQueryParam]="syncStateWithQueryParam"
56 [states]="dashboardConfiguration.states"> 57 [states]="dashboardConfiguration.states">
57 </tb-states-component> 58 </tb-states-component>
58 </div> 59 </div>
@@ -78,7 +79,7 @@ @@ -78,7 +79,7 @@
78 (click)="isFullscreen = !isFullscreen"> 79 (click)="isFullscreen = !isFullscreen">
79 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> 80 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
80 </button> 81 </button>
81 - <button [fxShow]="isEdit || displayExport()" mat-icon-button 82 + <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button
82 matTooltip="{{'dashboard.export' | translate}}" 83 matTooltip="{{'dashboard.export' | translate}}"
83 matTooltipPosition="below" 84 matTooltipPosition="below"
84 (click)="exportDashboard($event)"> 85 (click)="exportDashboard($event)">
@@ -118,7 +119,7 @@ @@ -118,7 +119,7 @@
118 (click)="openDashboardSettings($event)"> 119 (click)="openDashboardSettings($event)">
119 <mat-icon>settings</mat-icon> 120 <mat-icon>settings</mat-icon>
120 </button> 121 </button>
121 - <tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && displayDashboardsSelect()" 122 + <tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()"
122 [(ngModel)]="currentDashboardId" 123 [(ngModel)]="currentDashboardId"
123 (ngModelChange)="currentDashboardIdChanged(currentDashboardId)" 124 (ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
124 [customerId]="currentCustomerId" 125 [customerId]="currentCustomerId"
@@ -124,6 +124,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -124,6 +124,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
124 hideToolbar: boolean; 124 hideToolbar: boolean;
125 125
126 @Input() 126 @Input()
  127 + syncStateWithQueryParam = true;
  128 +
  129 + @Input()
127 dashboard: Dashboard; 130 dashboard: Dashboard;
128 dashboardConfiguration: DashboardConfiguration; 131 dashboardConfiguration: DashboardConfiguration;
129 132
@@ -266,9 +269,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -266,9 +269,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
266 this.rxSubscriptions.push(this.route.data.subscribe( 269 this.rxSubscriptions.push(this.route.data.subscribe(
267 (data) => { 270 (data) => {
268 if (this.embedded) { 271 if (this.embedded) {
269 - data.dashboard = this.dashboard; 272 + data.dashboard = this.dashboardUtils.validateAndUpdateDashboard(this.dashboard);
  273 + data.currentDashboardId = this.dashboard.id ? this.dashboard.id.id : null;
270 data.widgetEditMode = false; 274 data.widgetEditMode = false;
271 data.singlePageMode = false; 275 data.singlePageMode = false;
  276 + } else {
  277 + data.currentDashboardId = this.route.snapshot.params.dashboardId;
272 } 278 }
273 this.init(data); 279 this.init(data);
274 this.runChangeDetection(); 280 this.runChangeDetection();
@@ -286,7 +292,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -286,7 +292,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
286 292
287 this.reset(); 293 this.reset();
288 294
289 - this.currentDashboardId = this.route.snapshot.params.dashboardId; 295 + this.currentDashboardId = data.currentDashboardId;
290 296
291 if (this.route.snapshot.params.customerId) { 297 if (this.route.snapshot.params.customerId) {
292 this.currentCustomerId = this.route.snapshot.params.customerId; 298 this.currentCustomerId = this.route.snapshot.params.customerId;
@@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
23 [widgetLayouts]="{}" 23 [widgetLayouts]="{}"
24 [isEdit]="false" 24 [isEdit]="false"
25 [isMobile]="true" 25 [isMobile]="true"
  26 + [disableWidgetInteraction]="true"
26 [isEditActionEnabled]="false" 27 [isEditActionEnabled]="false"
27 [isExportActionEnabled]="false" 28 [isExportActionEnabled]="false"
28 [isRemoveActionEnabled]="false" 29 [isRemoveActionEnabled]="false"
@@ -34,6 +35,7 @@ @@ -34,6 +35,7 @@
34 [widgetLayouts]="{}" 35 [widgetLayouts]="{}"
35 [isEdit]="false" 36 [isEdit]="false"
36 [isMobile]="true" 37 [isMobile]="true"
  38 + [disableWidgetInteraction]="true"
37 [isEditActionEnabled]="false" 39 [isEditActionEnabled]="false"
38 [isExportActionEnabled]="false" 40 [isExportActionEnabled]="false"
39 [isRemoveActionEnabled]="false" 41 [isRemoveActionEnabled]="false"
@@ -45,6 +47,7 @@ @@ -45,6 +47,7 @@
45 [widgetLayouts]="{}" 47 [widgetLayouts]="{}"
46 [isEdit]="false" 48 [isEdit]="false"
47 [isMobile]="true" 49 [isMobile]="true"
  50 + [disableWidgetInteraction]="true"
48 [isEditActionEnabled]="false" 51 [isEditActionEnabled]="false"
49 [isExportActionEnabled]="false" 52 [isExportActionEnabled]="false"
50 [isRemoveActionEnabled]="false" 53 [isRemoveActionEnabled]="false"
@@ -56,6 +59,7 @@ @@ -56,6 +59,7 @@
56 [widgetLayouts]="{}" 59 [widgetLayouts]="{}"
57 [isEdit]="false" 60 [isEdit]="false"
58 [isMobile]="true" 61 [isMobile]="true"
  62 + [disableWidgetInteraction]="true"
59 [isEditActionEnabled]="false" 63 [isEditActionEnabled]="false"
60 [isExportActionEnabled]="false" 64 [isExportActionEnabled]="false"
61 [isRemoveActionEnabled]="false" 65 [isRemoveActionEnabled]="false"
@@ -67,6 +71,7 @@ @@ -67,6 +71,7 @@
67 [widgetLayouts]="{}" 71 [widgetLayouts]="{}"
68 [isEdit]="false" 72 [isEdit]="false"
69 [isMobile]="true" 73 [isMobile]="true"
  74 + [disableWidgetInteraction]="true"
70 [isEditActionEnabled]="false" 75 [isEditActionEnabled]="false"
71 [isExportActionEnabled]="false" 76 [isExportActionEnabled]="false"
72 [isRemoveActionEnabled]="false" 77 [isRemoveActionEnabled]="false"
@@ -53,6 +53,7 @@ @@ -53,6 +53,7 @@
53 [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight" 53 [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
54 [isMobile]="isMobile" 54 [isMobile]="isMobile"
55 [isMobileDisabled]="widgetEditMode" 55 [isMobileDisabled]="widgetEditMode"
  56 + [disableWidgetInteraction]="isEdit"
56 [isEditActionEnabled]="isEdit" 57 [isEditActionEnabled]="isEdit"
57 [isExportActionEnabled]="isEdit && !widgetEditMode" 58 [isExportActionEnabled]="isEdit && !widgetEditMode"
58 [isRemoveActionEnabled]="isEdit && !widgetEditMode" 59 [isRemoveActionEnabled]="isEdit && !widgetEditMode"
@@ -88,6 +88,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon @@ -88,6 +88,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon
88 88
89 currentState: string; 89 currentState: string;
90 90
  91 + syncStateWithQueryParam: boolean;
  92 +
91 private rxSubscriptions = new Array<Subscription>(); 93 private rxSubscriptions = new Array<Subscription>();
92 94
93 private inited = false; 95 private inited = false;
@@ -99,18 +101,20 @@ export abstract class StateControllerComponent implements IStateControllerCompon @@ -99,18 +101,20 @@ export abstract class StateControllerComponent implements IStateControllerCompon
99 } 101 }
100 102
101 ngOnInit(): void { 103 ngOnInit(): void {
102 - this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {  
103 - const dashboardId = this.route.snapshot.params.dashboardId || '';  
104 - if (this.dashboardId === dashboardId) {  
105 - const newState = this.decodeStateParam(paramMap.get('state'));  
106 - if (this.currentState !== newState) {  
107 - this.currentState = newState;  
108 - if (this.inited) {  
109 - this.onStateChanged(); 104 + if (this.syncStateWithQueryParam) {
  105 + this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
  106 + const dashboardId = this.route.snapshot.params.dashboardId || '';
  107 + if (this.dashboardId === dashboardId) {
  108 + const newState = this.decodeStateParam(paramMap.get('state'));
  109 + if (this.currentState !== newState) {
  110 + this.currentState = newState;
  111 + if (this.inited) {
  112 + this.onStateChanged();
  113 + }
110 } 114 }
111 } 115 }
112 - }  
113 - })); 116 + }));
  117 + }
114 this.init(); 118 this.init();
115 this.inited = true; 119 this.inited = true;
116 } 120 }
@@ -124,16 +128,18 @@ export abstract class StateControllerComponent implements IStateControllerCompon @@ -124,16 +128,18 @@ export abstract class StateControllerComponent implements IStateControllerCompon
124 128
125 protected updateStateParam(newState: string) { 129 protected updateStateParam(newState: string) {
126 this.currentState = newState; 130 this.currentState = newState;
127 - const queryParams: Params = { state: this.currentState };  
128 - this.ngZone.run(() => {  
129 - this.router.navigate(  
130 - [],  
131 - {  
132 - relativeTo: this.route,  
133 - queryParams,  
134 - queryParamsHandling: 'merge',  
135 - });  
136 - }); 131 + if (this.syncStateWithQueryParam) {
  132 + const queryParams: Params = {state: this.currentState};
  133 + this.ngZone.run(() => {
  134 + this.router.navigate(
  135 + [],
  136 + {
  137 + relativeTo: this.route,
  138 + queryParams,
  139 + queryParamsHandling: 'merge',
  140 + });
  141 + });
  142 + }
137 } 143 }
138 144
139 public openRightLayout(): void { 145 public openRightLayout(): void {
@@ -24,6 +24,7 @@ export interface IStateControllerComponent extends IStateController { @@ -24,6 +24,7 @@ export interface IStateControllerComponent extends IStateController {
24 stateControllerInstanceId: string; 24 stateControllerInstanceId: string;
25 state: string; 25 state: string;
26 currentState: string; 26 currentState: string;
  27 + syncStateWithQueryParam: boolean;
27 isMobile: boolean; 28 isMobile: boolean;
28 states: {[id: string]: DashboardState }; 29 states: {[id: string]: DashboardState };
29 dashboardId: string; 30 dashboardId: string;
@@ -54,6 +54,9 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { @@ -54,6 +54,9 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
54 currentState: string; 54 currentState: string;
55 55
56 @Input() 56 @Input()
  57 + syncStateWithQueryParam: boolean;
  58 +
  59 + @Input()
57 isMobile: boolean; 60 isMobile: boolean;
58 61
59 stateControllerComponentRef: ComponentRef<IStateControllerComponent>; 62 stateControllerComponentRef: ComponentRef<IStateControllerComponent>;
@@ -89,6 +92,8 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { @@ -89,6 +92,8 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
89 this.stateControllerComponent.state = this.state; 92 this.stateControllerComponent.state = this.state;
90 } else if (propName === 'currentState') { 93 } else if (propName === 'currentState') {
91 this.stateControllerComponent.currentState = this.currentState; 94 this.stateControllerComponent.currentState = this.currentState;
  95 + } else if (propName === 'syncStateWithQueryParam') {
  96 + this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam;
92 } 97 }
93 } 98 }
94 } 99 }
@@ -119,6 +124,7 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { @@ -119,6 +124,7 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
119 this.stateControllerComponent.stateControllerInstanceId = stateControllerInstanceId; 124 this.stateControllerComponent.stateControllerInstanceId = stateControllerInstanceId;
120 this.stateControllerComponent.state = this.state; 125 this.stateControllerComponent.state = this.state;
121 this.stateControllerComponent.currentState = this.currentState; 126 this.stateControllerComponent.currentState = this.currentState;
  127 + this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam;
122 this.stateControllerComponent.isMobile = this.isMobile; 128 this.stateControllerComponent.isMobile = this.isMobile;
123 this.stateControllerComponent.states = this.states; 129 this.stateControllerComponent.states = this.states;
124 this.stateControllerComponent.dashboardId = this.dashboardId; 130 this.stateControllerComponent.dashboardId = this.dashboardId;
@@ -150,7 +150,7 @@ @@ -150,7 +150,7 @@
150 </button> 150 </button>
151 </div> 151 </div>
152 </div> 152 </div>
153 - <div fxFlex fxLayout="column" class="tb-widget-content"> 153 + <div fxFlex fxLayout="column" class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">
154 <tb-widget fxFlex 154 <tb-widget fxFlex
155 #widgetComponent 155 #widgetComponent
156 [dashboardWidget]="widget" 156 [dashboardWidget]="widget"
@@ -128,6 +128,9 @@ div.tb-widget { @@ -128,6 +128,9 @@ div.tb-widget {
128 } 128 }
129 129
130 .tb-widget-content { 130 .tb-widget-content {
  131 + &.tb-no-interaction {
  132 + pointer-events: none;
  133 + }
131 tb-widget { 134 tb-widget {
132 position: relative; 135 position: relative;
133 width: 100%; 136 width: 100%;
@@ -114,6 +114,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -114,6 +114,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
114 isRemoveActionEnabled: boolean; 114 isRemoveActionEnabled: boolean;
115 115
116 @Input() 116 @Input()
  117 + disableWidgetInteraction = false;
  118 +
  119 + @Input()
117 dashboardStyle: {[klass: string]: any}; 120 dashboardStyle: {[klass: string]: any};
118 121
119 @Input() 122 @Input()
@@ -238,7 +238,8 @@ @@ -238,7 +238,8 @@
238 [length]="dataSource.total() | async" 238 [length]="dataSource.total() | async"
239 [pageIndex]="pageLink.page" 239 [pageIndex]="pageLink.page"
240 [pageSize]="pageLink.pageSize" 240 [pageSize]="pageLink.pageSize"
241 - [pageSizeOptions]="pageSizeOptions"></mat-paginator> 241 + [pageSizeOptions]="pageSizeOptions"
  242 + showFirstLastButtons></mat-paginator>
242 </div> 243 </div>
243 </div> 244 </div>
244 </mat-drawer-content> 245 </mat-drawer-content>
@@ -44,7 +44,7 @@ import { EntityAction } from '@home/models/entity/entity-component.models'; @@ -44,7 +44,7 @@ import { EntityAction } from '@home/models/entity/entity-component.models';
44 import { Subscription } from 'rxjs'; 44 import { Subscription } from 'rxjs';
45 import { MatTab, MatTabGroup } from '@angular/material/tabs'; 45 import { MatTab, MatTabGroup } from '@angular/material/tabs';
46 import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; 46 import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
47 -import { deepClone } from '@core/utils'; 47 +import { deepClone, mergeDeep } from '@core/utils';
48 48
49 @Component({ 49 @Component({
50 selector: 'tb-entity-details-panel', 50 selector: 'tb-entity-details-panel',
@@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit @@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
280 280
281 saveEntity() { 281 saveEntity() {
282 if (this.detailsForm.valid) { 282 if (this.detailsForm.valid) {
283 - const editingEntity = {...this.editingEntity, ...this.entityComponent.entityFormValue()}; 283 + const editingEntity = mergeDeep(this.editingEntity, this.entityComponent.entityFormValue());
284 this.entitiesTableConfig.saveEntity(editingEntity).subscribe( 284 this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
285 (entity) => { 285 (entity) => {
286 this.entity = entity; 286 this.entity = entity;
@@ -14,7 +14,16 @@ @@ -14,7 +14,16 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 17 +import {
  18 + Component,
  19 + Injector,
  20 + Input,
  21 + OnDestroy,
  22 + OnInit,
  23 + StaticProvider,
  24 + ViewChild,
  25 + ViewContainerRef
  26 +} from '@angular/core';
18 import { TooltipPosition } from '@angular/material/tooltip'; 27 import { TooltipPosition } from '@angular/material/tooltip';
19 import { IAliasController } from '@core/api/widget-api.models'; 28 import { IAliasController } from '@core/api/widget-api.models';
20 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 29 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@@ -28,7 +37,7 @@ import { @@ -28,7 +37,7 @@ import {
28 FiltersEditPanelComponent, 37 FiltersEditPanelComponent,
29 FiltersEditPanelData 38 FiltersEditPanelData
30 } from '@home/components/filter/filters-edit-panel.component'; 39 } from '@home/components/filter/filters-edit-panel.component';
31 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 40 +import { ComponentPortal } from '@angular/cdk/portal';
32 import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component'; 41 import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component';
33 import { MatDialog } from '@angular/material/dialog'; 42 import { MatDialog } from '@angular/material/dialog';
34 43
@@ -153,12 +162,12 @@ export class FiltersEditComponent implements OnInit, OnDestroy { @@ -153,12 +162,12 @@ export class FiltersEditComponent implements OnInit, OnDestroy {
153 } 162 }
154 } 163 }
155 164
156 - private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): PortalInjector {  
157 - const injectionTokens = new WeakMap<any, any>([  
158 - [FILTER_EDIT_PANEL_DATA, data],  
159 - [OverlayRef, overlayRef]  
160 - ]);  
161 - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); 165 + private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): Injector {
  166 + const providers: StaticProvider[] = [
  167 + {provide: FILTER_EDIT_PANEL_DATA, useValue: data},
  168 + {provide: OverlayRef, useValue: overlayRef}
  169 + ];
  170 + return Injector.create({parent: this.viewContainerRef.injector, providers});
162 } 171 }
163 172
164 private updateFiltersInfo() { 173 private updateFiltersInfo() {
@@ -67,15 +67,13 @@ @@ -67,15 +67,13 @@
67 </mat-form-field> 67 </mat-form-field>
68 <section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value"> 68 <section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value">
69 <ng-template [ngSwitchCase]="widgetActionType.openDashboard"> 69 <ng-template [ngSwitchCase]="widgetActionType.openDashboard">
70 - <div fxLayout="column">  
71 - <div class="mat-caption tb-required"  
72 - style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>  
73 - <tb-dashboard-autocomplete  
74 - formControlName="targetDashboardId"  
75 - required  
76 - [selectFirstDashboard]="true"  
77 - ></tb-dashboard-autocomplete>  
78 - </div> 70 + <div class="mat-caption tb-required"
  71 + style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
  72 + <tb-dashboard-autocomplete
  73 + formControlName="targetDashboardId"
  74 + required
  75 + [selectFirstDashboard]="true"
  76 + ></tb-dashboard-autocomplete>
79 </ng-template> 77 </ng-template>
80 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || 78 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
81 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || 79 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
@@ -122,26 +120,24 @@ @@ -122,26 +120,24 @@
122 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || 120 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
123 widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? 121 widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
124 widgetActionFormGroup.get('type').value : ''"> 122 widgetActionFormGroup.get('type').value : ''">
125 - <div fxFlex fxLayout="column">  
126 - <mat-checkbox formControlName="setEntityId">  
127 - {{ 'widget-action.set-entity-from-widget' | translate }}  
128 - </mat-checkbox>  
129 - <mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"  
130 - floatLabel="always"  
131 - class="mat-block">  
132 - <mat-label translate>alias.state-entity-parameter-name</mat-label>  
133 - <input matInput  
134 - placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"  
135 - formControlName="stateEntityParamName">  
136 - </mat-form-field>  
137 - </div> 123 + <mat-checkbox formControlName="setEntityId">
  124 + {{ 'widget-action.set-entity-from-widget' | translate }}
  125 + </mat-checkbox>
  126 + <mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"
  127 + floatLabel="always"
  128 + class="mat-block">
  129 + <mat-label translate>alias.state-entity-parameter-name</mat-label>
  130 + <input matInput
  131 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  132 + formControlName="stateEntityParamName">
  133 + </mat-form-field>
138 </ng-template> 134 </ng-template>
139 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ? 135 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ?
140 widgetActionFormGroup.get('type').value : ''"> 136 widgetActionFormGroup.get('type').value : ''">
141 <mat-checkbox formControlName="openInSeparateDialog"> 137 <mat-checkbox formControlName="openInSeparateDialog">
142 {{ 'widget-action.open-in-separate-dialog' | translate }} 138 {{ 'widget-action.open-in-separate-dialog' | translate }}
143 </mat-checkbox> 139 </mat-checkbox>
144 - <section fxLayout="column" *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value"> 140 + <section *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value">
145 <mat-form-field class="mat-block"> 141 <mat-form-field class="mat-block">
146 <mat-label translate>widget-action.dialog-title</mat-label> 142 <mat-label translate>widget-action.dialog-title</mat-label>
147 <input matInput formControlName="dialogTitle"> 143 <input matInput formControlName="dialogTitle">
@@ -27,7 +27,8 @@ @@ -27,7 +27,8 @@
27 </mat-progress-bar> 27 </mat-progress-bar>
28 <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> 28 <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
29 <div class="dashboard-state-dialog-content" mat-dialog-content fxFlex fxLayout="column" style="padding: 8px;"> 29 <div class="dashboard-state-dialog-content" mat-dialog-content fxFlex fxLayout="column" style="padding: 8px;">
30 - <tb-dashboard-page [embedded]="true" [hideToolbar]="hideToolbar" [currentState]="state" [dashboard]="dashboard" style="width: 100%; height: 100%;"></tb-dashboard-page> 30 + <tb-dashboard-page [embedded]="true" [syncStateWithQueryParam]="false" [hideToolbar]="hideToolbar"
  31 + [currentState]="state" [dashboard]="dashboard" style="width: 100%; height: 100%;"></tb-dashboard-page>
31 </div> 32 </div>
32 <div mat-dialog-actions fxLayoutAlign="end center"> 33 <div mat-dialog-actions fxLayoutAlign="end center">
33 <button mat-button color="primary" 34 <button mat-button color="primary"
@@ -14,11 +14,22 @@ @@ -14,11 +14,22 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 17 +import {
  18 + Component,
  19 + forwardRef,
  20 + Inject,
  21 + Injector,
  22 + Input,
  23 + OnDestroy,
  24 + OnInit,
  25 + StaticProvider,
  26 + ViewChild,
  27 + ViewContainerRef
  28 +} from '@angular/core';
18 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 29 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 import { DOCUMENT } from '@angular/common'; 30 import { DOCUMENT } from '@angular/common';
20 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 31 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
21 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 32 +import { ComponentPortal } from '@angular/cdk/portal';
22 import { MediaBreakpoints } from '@shared/models/constants'; 33 import { MediaBreakpoints } from '@shared/models/constants';
23 import { BreakpointObserver } from '@angular/cdk/layout'; 34 import { BreakpointObserver } from '@angular/cdk/layout';
24 import { WINDOW } from '@core/services/window.service'; 35 import { WINDOW } from '@core/services/window.service';
@@ -140,12 +151,12 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc @@ -140,12 +151,12 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
140 overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector)); 151 overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector));
141 } 152 }
142 153
143 - private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector {  
144 - const injectionTokens = new WeakMap<any, any>([  
145 - [LEGEND_CONFIG_PANEL_DATA, data],  
146 - [OverlayRef, overlayRef]  
147 - ]);  
148 - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); 154 + private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): Injector {
  155 + const providers: StaticProvider[] = [
  156 + {provide: LEGEND_CONFIG_PANEL_DATA, useValue: data},
  157 + {provide: OverlayRef, useValue: overlayRef}
  158 + ];
  159 + return Injector.create({parent: this.viewContainerRef.injector, providers});
149 } 160 }
150 161
151 registerOnChange(fn: any): void { 162 registerOnChange(fn: any): void {
@@ -140,6 +140,7 @@ @@ -140,6 +140,7 @@
140 [length]="alarmsDatasource.total() | async" 140 [length]="alarmsDatasource.total() | async"
141 [pageIndex]="pageLink.page" 141 [pageIndex]="pageLink.page"
142 [pageSize]="pageLink.pageSize" 142 [pageSize]="pageLink.pageSize"
143 - [pageSizeOptions]="pageSizeOptions"></mat-paginator> 143 + [pageSizeOptions]="pageSizeOptions"
  144 + showFirstLastButtons></mat-paginator>
144 </div> 145 </div>
145 </div> 146 </div>
@@ -19,9 +19,11 @@ import { @@ -19,9 +19,11 @@ import {
19 Component, 19 Component,
20 ElementRef, 20 ElementRef,
21 EventEmitter, 21 EventEmitter,
  22 + Injector,
22 Input, 23 Input,
23 NgZone, 24 NgZone,
24 OnInit, 25 OnInit,
  26 + StaticProvider,
25 ViewChild, 27 ViewChild,
26 ViewContainerRef 28 ViewContainerRef
27 } from '@angular/core'; 29 } from '@angular/core';
@@ -64,7 +66,7 @@ import { @@ -64,7 +66,7 @@ import {
64 widthStyle 66 widthStyle
65 } from '@home/components/widget/lib/table-widget.models'; 67 } from '@home/components/widget/lib/table-widget.models';
66 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 68 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
67 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 69 +import { ComponentPortal } from '@angular/cdk/portal';
68 import { 70 import {
69 DISPLAY_COLUMNS_PANEL_DATA, 71 DISPLAY_COLUMNS_PANEL_DATA,
70 DisplayColumnsPanelComponent, 72 DisplayColumnsPanelComponent,
@@ -452,20 +454,26 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, @@ -452,20 +454,26 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
452 }; 454 };
453 }); 455 });
454 456
455 - const injectionTokens = new WeakMap<any, any>([  
456 - [DISPLAY_COLUMNS_PANEL_DATA, {  
457 - columns,  
458 - columnsUpdated: (newColumns) => {  
459 - this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);  
460 - if (this.enableSelection) {  
461 - this.displayedColumns.unshift('select'); 457 + const providers: StaticProvider[] = [
  458 + {
  459 + provide: DISPLAY_COLUMNS_PANEL_DATA,
  460 + useValue: {
  461 + columns,
  462 + columnsUpdated: (newColumns) => {
  463 + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
  464 + if (this.enableSelection) {
  465 + this.displayedColumns.unshift('select');
  466 + }
  467 + this.displayedColumns.push('actions');
462 } 468 }
463 - this.displayedColumns.push('actions');  
464 - }  
465 - } as DisplayColumnsPanelData],  
466 - [OverlayRef, overlayRef]  
467 - ]);  
468 - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); 469 + } as DisplayColumnsPanelData
  470 + },
  471 + {
  472 + provide: OverlayRef,
  473 + useValue: overlayRef
  474 + }
  475 + ];
  476 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
469 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, 477 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
470 this.viewContainerRef, injector)); 478 this.viewContainerRef, injector));
471 this.ctx.detectChanges(); 479 this.ctx.detectChanges();
@@ -492,15 +500,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, @@ -492,15 +500,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
492 overlayRef.backdropClick().subscribe(() => { 500 overlayRef.backdropClick().subscribe(() => {
493 overlayRef.dispose(); 501 overlayRef.dispose();
494 }); 502 });
495 - const injectionTokens = new WeakMap<any, any>([  
496 - [ALARM_FILTER_PANEL_DATA, {  
497 - statusList: this.pageLink.statusList,  
498 - severityList: this.pageLink.severityList,  
499 - typeList: this.pageLink.typeList  
500 - } as AlarmFilterPanelData],  
501 - [OverlayRef, overlayRef]  
502 - ]);  
503 - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); 503 + const providers: StaticProvider[] = [
  504 + {
  505 + provide: ALARM_FILTER_PANEL_DATA,
  506 + useValue: {
  507 + statusList: this.pageLink.statusList,
  508 + severityList: this.pageLink.severityList,
  509 + typeList: this.pageLink.typeList
  510 + } as AlarmFilterPanelData
  511 + },
  512 + {
  513 + provide: OverlayRef,
  514 + useValue: overlayRef
  515 + }
  516 + ];
  517 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
504 const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent, 518 const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent,
505 this.viewContainerRef, injector)); 519 this.viewContainerRef, injector));
506 componentRef.onDestroy(() => { 520 componentRef.onDestroy(() => {
@@ -18,9 +18,11 @@ import { @@ -18,9 +18,11 @@ import {
18 Component, 18 Component,
19 Inject, 19 Inject,
20 InjectionToken, 20 InjectionToken,
  21 + Injector,
21 Input, 22 Input,
22 OnDestroy, 23 OnDestroy,
23 OnInit, 24 OnInit,
  25 + StaticProvider,
24 ViewChild, 26 ViewChild,
25 ViewContainerRef, 27 ViewContainerRef,
26 ViewEncapsulation 28 ViewEncapsulation
@@ -41,7 +43,7 @@ import { @@ -41,7 +43,7 @@ import {
41 import { KeyValue } from '@angular/common'; 43 import { KeyValue } from '@angular/common';
42 import * as _moment from 'moment'; 44 import * as _moment from 'moment';
43 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 45 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
44 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 46 +import { ComponentPortal } from '@angular/cdk/portal';
45 import { MatSelect } from '@angular/material/select'; 47 import { MatSelect } from '@angular/material/select';
46 import { Subscription } from 'rxjs'; 48 import { Subscription } from 'rxjs';
47 import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.models'; 49 import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.models';
@@ -142,18 +144,24 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements @@ -142,18 +144,24 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements
142 overlayRef.backdropClick().subscribe(() => { 144 overlayRef.backdropClick().subscribe(() => {
143 overlayRef.dispose(); 145 overlayRef.dispose();
144 }); 146 });
145 - const injectionTokens = new WeakMap<any, any>([  
146 - [DATE_RANGE_NAVIGATOR_PANEL_DATA, {  
147 - model: cloneDateRangeNavigatorModel(this.advancedModel),  
148 - settings: this.settings,  
149 - onChange: model => {  
150 - this.advancedModel = model;  
151 - this.triggerChange();  
152 - }  
153 - } as DateRangeNavigatorPanelData],  
154 - [OverlayRef, overlayRef]  
155 - ]);  
156 - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); 147 + const providers: StaticProvider[] = [
  148 + {
  149 + provide: DATE_RANGE_NAVIGATOR_PANEL_DATA,
  150 + useValue: {
  151 + model: cloneDateRangeNavigatorModel(this.advancedModel),
  152 + settings: this.settings,
  153 + onChange: model => {
  154 + this.advancedModel = model;
  155 + this.triggerChange();
  156 + }
  157 + } as DateRangeNavigatorPanelData
  158 + },
  159 + {
  160 + provide: OverlayRef,
  161 + useValue: overlayRef
  162 + }
  163 + ];
  164 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
157 overlayRef.attach(new ComponentPortal(DateRangeNavigatorPanelComponent, 165 overlayRef.attach(new ComponentPortal(DateRangeNavigatorPanelComponent,
158 this.viewContainerRef, injector)); 166 this.viewContainerRef, injector));
159 this.ctx.detectChanges(); 167 this.ctx.detectChanges();
@@ -99,6 +99,7 @@ @@ -99,6 +99,7 @@
99 [length]="entityDatasource.total() | async" 99 [length]="entityDatasource.total() | async"
100 [pageIndex]="pageLink.page" 100 [pageIndex]="pageLink.page"
101 [pageSize]="pageLink.pageSize" 101 [pageSize]="pageLink.pageSize"
102 - [pageSizeOptions]="pageSizeOptions"></mat-paginator> 102 + [pageSizeOptions]="pageSizeOptions"
  103 + showFirstLastButtons></mat-paginator>
103 </div> 104 </div>
104 </div> 105 </div>
@@ -18,9 +18,11 @@ import { @@ -18,9 +18,11 @@ import {
18 AfterViewInit, 18 AfterViewInit,
19 Component, 19 Component,
20 ElementRef, 20 ElementRef,
  21 + Injector,
21 Input, 22 Input,
22 NgZone, 23 NgZone,
23 OnInit, 24 OnInit,
  25 + StaticProvider,
24 ViewChild, 26 ViewChild,
25 ViewContainerRef 27 ViewContainerRef
26 } from '@angular/core'; 28 } from '@angular/core';
@@ -70,7 +72,7 @@ import { @@ -70,7 +72,7 @@ import {
70 widthStyle 72 widthStyle
71 } from '@home/components/widget/lib/table-widget.models'; 73 } from '@home/components/widget/lib/table-widget.models';
72 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 74 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
73 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 75 +import { ComponentPortal } from '@angular/cdk/portal';
74 import { 76 import {
75 DISPLAY_COLUMNS_PANEL_DATA, 77 DISPLAY_COLUMNS_PANEL_DATA,
76 DisplayColumnsPanelComponent, 78 DisplayColumnsPanelComponent,
@@ -422,17 +424,23 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -422,17 +424,23 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
422 }; 424 };
423 }); 425 });
424 426
425 - const injectionTokens = new WeakMap<any, any>([  
426 - [DISPLAY_COLUMNS_PANEL_DATA, {  
427 - columns,  
428 - columnsUpdated: (newColumns) => {  
429 - this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);  
430 - this.displayedColumns.push('actions');  
431 - }  
432 - } as DisplayColumnsPanelData],  
433 - [OverlayRef, overlayRef]  
434 - ]);  
435 - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); 427 + const providers: StaticProvider[] = [
  428 + {
  429 + provide: DISPLAY_COLUMNS_PANEL_DATA,
  430 + useValue: {
  431 + columns,
  432 + columnsUpdated: (newColumns) => {
  433 + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
  434 + this.displayedColumns.push('actions');
  435 + }
  436 + } as DisplayColumnsPanelData
  437 + },
  438 + {
  439 + provide: OverlayRef,
  440 + useValue: overlayRef
  441 + }
  442 + ];
  443 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
436 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, 444 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
437 this.viewContainerRef, injector)); 445 this.viewContainerRef, injector));
438 this.ctx.detectChanges(); 446 this.ctx.detectChanges();
@@ -175,7 +175,7 @@ export class TbFlot { @@ -175,7 +175,7 @@ export class TbFlot {
175 autoHighlight: this.tooltipIndividual === true, 175 autoHighlight: this.tooltipIndividual === true,
176 markings: [] 176 markings: []
177 }, 177 },
178 - selection : { mode : ctx.isMobile ? null : 'x' }, 178 + selection : { mode : 'x' },
179 legend : { 179 legend : {
180 show: false 180 show: false
181 } 181 }
@@ -702,7 +702,7 @@ export class TbFlot { @@ -702,7 +702,7 @@ export class TbFlot {
702 } 702 }
703 703
704 public checkMouseEvents() { 704 public checkMouseEvents() {
705 - const enabled = !this.ctx.isMobile && !this.ctx.isEdit; 705 + const enabled = !this.ctx.isEdit;
706 if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) { 706 if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) {
707 this.mouseEventsEnabled = enabled; 707 this.mouseEventsEnabled = enabled;
708 if (this.$element) { 708 if (this.$element) {
@@ -626,10 +626,14 @@ export default abstract class LeafletMap { @@ -626,10 +626,14 @@ export default abstract class LeafletMap {
626 } 626 }
627 this.points = new FeatureGroup(); 627 this.points = new FeatureGroup();
628 } 628 }
  629 + let pointColor = this.options.pointColor;
629 for (const pointsList of pointsData) { 630 for (const pointsList of pointsData) {
630 pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { 631 pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => {
  632 + if (this.options.useColorPointFunction) {
  633 + pointColor = safeExecute(this.options.colorPointFunction, [data, pointsData, data.dsIndex]);
  634 + }
631 const point = L.circleMarker(this.convertPosition(data), { 635 const point = L.circleMarker(this.convertPosition(data), {
632 - color: this.options.pointColor, 636 + color: pointColor,
633 radius: this.options.pointSize 637 radius: this.options.pointSize
634 }); 638 });
635 if (!this.options.pointTooltipOnRightPanel) { 639 if (!this.options.pointTooltipOnRightPanel) {
@@ -201,6 +201,8 @@ export type TripAnimationSettings = { @@ -201,6 +201,8 @@ export type TripAnimationSettings = {
201 pointAsAnchorFunction: GenericFunction; 201 pointAsAnchorFunction: GenericFunction;
202 tooltipFunction: GenericFunction; 202 tooltipFunction: GenericFunction;
203 labelFunction: GenericFunction; 203 labelFunction: GenericFunction;
  204 + useColorPointFunction: boolean;
  205 + colorPointFunction: GenericFunction;
204 }; 206 };
205 207
206 export type actionsHandler = ($event: Event, datasource: Datasource) => void; 208 export type actionsHandler = ($event: Event, datasource: Datasource) => void;
@@ -301,6 +301,7 @@ export class MapWidgetController implements MapWidgetInterface { @@ -301,6 +301,7 @@ export class MapWidgetController implements MapWidgetInterface {
301 labelFunction: parseFunction(settings.labelFunction, functionParams), 301 labelFunction: parseFunction(settings.labelFunction, functionParams),
302 tooltipFunction: parseFunction(settings.tooltipFunction, functionParams), 302 tooltipFunction: parseFunction(settings.tooltipFunction, functionParams),
303 colorFunction: parseFunction(settings.colorFunction, functionParams), 303 colorFunction: parseFunction(settings.colorFunction, functionParams),
  304 + colorPointFunction: parseFunction(settings.colorPointFunction, functionParams),
304 polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), 305 polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams),
305 polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams), 306 polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams),
306 markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), 307 markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']),
@@ -871,6 +871,15 @@ export const pointSchema = @@ -871,6 +871,15 @@ export const pointSchema =
871 title: 'Point color', 871 title: 'Point color',
872 type: 'string' 872 type: 'string'
873 }, 873 },
  874 + useColorPointFunction: {
  875 + title: 'Use color point function',
  876 + type: 'boolean',
  877 + default: false
  878 + },
  879 + colorPointFunction: {
  880 + title: 'Color point function: f(data, dsData, dsIndex)',
  881 + type: 'string'
  882 + },
874 pointSize: { 883 pointSize: {
875 title: 'Point size (px)', 884 title: 'Point size (px)',
876 type: 'number', 885 type: 'number',
@@ -899,6 +908,11 @@ export const pointSchema = @@ -899,6 +908,11 @@ export const pointSchema =
899 key: 'pointColor', 908 key: 'pointColor',
900 type: 'color' 909 type: 'color'
901 }, 910 },
  911 + 'useColorPointFunction',
  912 + {
  913 + key: 'colorPointFunction',
  914 + type: 'javascript'
  915 + },
902 'pointSize', 916 'pointSize',
903 'usePointAsAnchor', 917 'usePointAsAnchor',
904 { 918 {
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<a mat-raised-button color="primary" class="tb-nav-button" href="{{settings.path}}" (click)="navigate($event, settings.path)">
  19 + <mat-icon class="material-icons tb-mat-96">{{settings.icon}}</mat-icon>
  20 + <span>{{translatedName}}</span>
  21 +</a>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 + width: 100%;
  19 + height: 100%;
  20 +}
  21 +
  22 +:host ::ng-deep {
  23 + .tb-nav-button {
  24 + width: 100%;
  25 + height: 100%;
  26 + &:hover {
  27 + border-bottom: none;
  28 + }
  29 + &:focus {
  30 + border-bottom: none;
  31 + }
  32 + .mat-button-wrapper {
  33 + width: 100%;
  34 + height: 100%;
  35 + display: flex;
  36 + flex-direction: column;
  37 + align-items: center;
  38 + mat-icon {
  39 + margin: auto;
  40 + }
  41 + span {
  42 + height: 18px;
  43 + min-height: 36px;
  44 + max-height: 36px;
  45 + padding: 0 0 20px 0;
  46 + margin: auto;
  47 + font-size: 18px;
  48 + font-weight: 400;
  49 + line-height: 18px;
  50 + white-space: normal;
  51 + }
  52 + }
  53 + &.mat-raised-button.mat-primary {
  54 + .mat-ripple-element {
  55 + opacity: 0.3;
  56 + background-color: rgba(255, 255, 255, 0.3);
  57 + }
  58 + }
  59 + }
  60 +}
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { PageComponent } from '@shared/components/page.component';
  18 +import { Component, Input, NgZone, OnInit } from '@angular/core';
  19 +import { WidgetContext } from '@home/models/widget-component.models';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { Router } from '@angular/router';
  23 +import { UtilsService } from '@core/services/utils.service';
  24 +
  25 +interface NavigationCardWidgetSettings {
  26 + name: string;
  27 + icon: string;
  28 + path: string;
  29 +}
  30 +
  31 +@Component({
  32 + selector: 'tb-navigation-card-widget',
  33 + templateUrl: './navigation-card-widget.component.html',
  34 + styleUrls: ['./navigation-card-widget.component.scss']
  35 +})
  36 +export class NavigationCardWidgetComponent extends PageComponent implements OnInit {
  37 +
  38 + settings: NavigationCardWidgetSettings;
  39 +
  40 + translatedName: string;
  41 +
  42 + @Input()
  43 + ctx: WidgetContext;
  44 +
  45 + constructor(protected store: Store<AppState>,
  46 + private utils: UtilsService,
  47 + private ngZone: NgZone,
  48 + private router: Router) {
  49 + super(store);
  50 + }
  51 +
  52 + ngOnInit(): void {
  53 + this.ctx.$scope.navigationCardWidget = this;
  54 + this.settings = this.ctx.settings;
  55 + this.translatedName = this.utils.customTranslation(this.settings.name, this.settings.name);
  56 + }
  57 +
  58 +
  59 + navigate($event: Event, path: string) {
  60 + $event.preventDefault();
  61 + this.ngZone.run(() => {
  62 + this.router.navigateByUrl(path);
  63 + });
  64 + }
  65 +
  66 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-grid-list *ngIf="cols" class="tb-navigation-cards" [cols]="cols" rowHeight="280px">
  19 + <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of showHomeSections$| async">
  20 + <mat-card style="width: 100%;">
  21 + <mat-card-title>
  22 + <span translate class="mat-headline">{{section.name}}</span>
  23 + </mat-card-title>
  24 + <mat-card-content>
  25 + <mat-grid-list rowHeight="170px" [cols]="sectionPlaces(section).length">
  26 + <mat-grid-tile *ngFor="let place of sectionPlaces(section)">
  27 + <a mat-raised-button color="primary" class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
  28 + <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
  29 + <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
  30 + <span translate>{{place.name}}</span>
  31 + </a>
  32 + </mat-grid-tile>
  33 + </mat-grid-list>
  34 + </mat-card-content>
  35 + </mat-card>
  36 + </mat-grid-tile>
  37 +</mat-grid-list>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import '../../../../../../scss/constants';
  17 +
  18 +:host {
  19 + width: 100%;
  20 + height: 100%;
  21 +}
  22 +
  23 +:host ::ng-deep {
  24 + .tb-navigation-cards {
  25 + .mat-headline {
  26 + font-size: 20px;
  27 + @media #{$mat-gt-xmd} {
  28 + font-size: 24px;
  29 + }
  30 + }
  31 + mat-card {
  32 + padding: 0;
  33 + margin: 8px;
  34 + mat-card-title {
  35 + margin: 0;
  36 + padding: 24px 16px 16px;
  37 + }
  38 + mat-card-title+mat-card-content {
  39 + padding-top: 0;
  40 + }
  41 + mat-card-content {
  42 + padding: 16px;
  43 + }
  44 + }
  45 + .tb-card-button {
  46 + width: 100%;
  47 + height: 100%;
  48 + max-width: 240px;
  49 + &:hover {
  50 + border-bottom: none;
  51 + }
  52 + &:focus {
  53 + border-bottom: none;
  54 + }
  55 + .mat-button-wrapper {
  56 + width: 100%;
  57 + height: 100%;
  58 + display: flex;
  59 + flex-direction: column;
  60 + align-items: center;
  61 + mat-icon {
  62 + margin: auto;
  63 + }
  64 + span {
  65 + height: 18px;
  66 + min-height: 36px;
  67 + max-height: 36px;
  68 + padding: 0 0 20px 0;
  69 + margin: auto;
  70 + font-size: 18px;
  71 + font-weight: 400;
  72 + line-height: 18px;
  73 + white-space: normal;
  74 + }
  75 + }
  76 + &.mat-raised-button.mat-primary {
  77 + .mat-ripple-element {
  78 + opacity: 0.3;
  79 + background-color: rgba(255, 255, 255, 0.3);
  80 + }
  81 + }
  82 + }
  83 + }
  84 +}
  85 +
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { PageComponent } from '@shared/components/page.component';
  18 +import { Component, Input, NgZone, OnInit } from '@angular/core';
  19 +import { WidgetContext } from '@home/models/widget-component.models';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { MenuService } from '@core/services/menu.service';
  23 +import { HomeSection, HomeSectionPlace } from '@core/services/menu.models';
  24 +import { Router } from '@angular/router';
  25 +import { map } from 'rxjs/operators';
  26 +
  27 +interface NavigationCardsWidgetSettings {
  28 + filterType: 'all' | 'include' | 'exclude';
  29 + filter: string[];
  30 +}
  31 +
  32 +@Component({
  33 + selector: 'tb-navigation-cards-widget',
  34 + templateUrl: './navigation-cards-widget.component.html',
  35 + styleUrls: ['./navigation-cards-widget.component.scss']
  36 +})
  37 +export class NavigationCardsWidgetComponent extends PageComponent implements OnInit {
  38 +
  39 + homeSections$ = this.menuService.homeSections();
  40 + showHomeSections$ = this.homeSections$.pipe(
  41 + map((sections) => {
  42 + return sections.filter((section) => this.sectionPlaces(section).length > 0);
  43 + })
  44 + );
  45 +
  46 + cols = null;
  47 +
  48 + settings: NavigationCardsWidgetSettings;
  49 +
  50 + @Input()
  51 + ctx: WidgetContext;
  52 +
  53 + constructor(protected store: Store<AppState>,
  54 + private menuService: MenuService,
  55 + private ngZone: NgZone,
  56 + private router: Router) {
  57 + super(store);
  58 + }
  59 +
  60 + ngOnInit(): void {
  61 + this.ctx.$scope.navigationCardsWidget = this;
  62 + this.settings = this.ctx.settings;
  63 + }
  64 +
  65 + resize() {
  66 + this.updateColumnCount();
  67 + }
  68 +
  69 + private updateColumnCount() {
  70 + this.cols = 2;
  71 + const width = this.ctx.width;
  72 + if (width >= 1280) {
  73 + this.cols = 3;
  74 + if (width >= 1920) {
  75 + this.cols = 4;
  76 + }
  77 + }
  78 + this.ctx.detectChanges();
  79 + }
  80 +
  81 + navigate($event: Event, path: string) {
  82 + $event.preventDefault();
  83 + this.ngZone.run(() => {
  84 + this.router.navigateByUrl(path);
  85 + });
  86 + }
  87 +
  88 + sectionPlaces(section: HomeSection): HomeSectionPlace[] {
  89 + return section && section.places ? section.places.filter((place) => this.filterPlace(place)) : [];
  90 + }
  91 +
  92 + private filterPlace(place: HomeSectionPlace): boolean {
  93 + if (this.settings.filterType === 'include') {
  94 + return this.settings.filter.includes(place.path);
  95 + } else if (this.settings.filterType === 'exclude') {
  96 + return !this.settings.filter.includes(place.path);
  97 + }
  98 + return true;
  99 + }
  100 +
  101 + sectionColspan(section: HomeSection): number {
  102 + if (this.ctx.width >= 960) {
  103 + let colspan = this.cols;
  104 + const places = this.sectionPlaces(section);
  105 + if (places.length <= colspan) {
  106 + colspan = places.length;
  107 + }
  108 + return colspan;
  109 + } else {
  110 + return 2;
  111 + }
  112 + }
  113 +
  114 +}
@@ -104,7 +104,8 @@ @@ -104,7 +104,8 @@
104 [length]="source.timeseriesDatasource.total() | async" 104 [length]="source.timeseriesDatasource.total() | async"
105 [pageIndex]="source.pageLink.page" 105 [pageIndex]="source.pageLink.page"
106 [pageSize]="source.pageLink.pageSize" 106 [pageSize]="source.pageLink.pageSize"
107 - [pageSizeOptions]="pageSizeOptions"></mat-paginator> 107 + [pageSizeOptions]="pageSizeOptions"
  108 + showFirstLastButtons></mat-paginator>
108 </mat-tab> 109 </mat-tab>
109 </mat-tab-group> 110 </mat-tab-group>
110 </div> 111 </div>
@@ -210,7 +210,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @@ -210,7 +210,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
210 this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; 210 this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
211 this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false; 211 this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false;
212 this.showTimestamp = this.settings.showTimestamp !== false; 212 this.showTimestamp = this.settings.showTimestamp !== false;
213 - this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss'; 213 + this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.SSS';
214 214
215 const pageSize = this.settings.defaultPageSize; 215 const pageSize = this.settings.defaultPageSize;
216 if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { 216 if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
@@ -112,6 +112,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy @@ -112,6 +112,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
112 this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); 112 this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']);
113 this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); 113 this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']);
114 this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']); 114 this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']);
  115 + this.settings.colorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']);
115 this.normalizationStep = this.settings.normalizationStep; 116 this.normalizationStep = this.settings.normalizationStep;
116 const subscription = this.ctx.defaultSubscription; 117 const subscription = this.ctx.defaultSubscription;
117 subscription.callbacks.onDataUpdated = () => { 118 subscription.callbacks.onDataUpdated = () => {
@@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen @@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen
35 import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; 35 import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component';
36 import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; 36 import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
37 import { ImportExportService } from '@home/components/import-export/import-export.service'; 37 import { ImportExportService } from '@home/components/import-export/import-export.service';
  38 +import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
  39 +import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
38 40
39 @NgModule({ 41 @NgModule({
40 declarations: 42 declarations:
@@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor @@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
50 MultipleInputWidgetComponent, 52 MultipleInputWidgetComponent,
51 TripAnimationComponent, 53 TripAnimationComponent,
52 PhotoCameraInputWidgetComponent, 54 PhotoCameraInputWidgetComponent,
53 - GatewayFormComponent 55 + GatewayFormComponent,
  56 + NavigationCardsWidgetComponent,
  57 + NavigationCardWidgetComponent
54 ], 58 ],
55 imports: [ 59 imports: [
56 CommonModule, 60 CommonModule,
@@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor @@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
68 MultipleInputWidgetComponent, 72 MultipleInputWidgetComponent,
69 TripAnimationComponent, 73 TripAnimationComponent,
70 PhotoCameraInputWidgetComponent, 74 PhotoCameraInputWidgetComponent,
71 - GatewayFormComponent 75 + GatewayFormComponent,
  76 + NavigationCardsWidgetComponent,
  77 + NavigationCardWidgetComponent
72 ], 78 ],
73 providers: [ 79 providers: [
74 CustomDialogService, 80 CustomDialogService,
@@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
32 import { OAuth2Service } from '@core/http/oauth2.service'; 32 import { OAuth2Service } from '@core/http/oauth2.service';
33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module'; 33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module';
34 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; 34 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
  35 +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
35 36
36 @Injectable() 37 @Injectable()
37 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { 38 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@@ -48,7 +49,7 @@ const routes: Routes = [ @@ -48,7 +49,7 @@ const routes: Routes = [
48 { 49 {
49 path: 'settings', 50 path: 'settings',
50 data: { 51 data: {
51 - auth: [Authority.SYS_ADMIN], 52 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
52 breadcrumb: { 53 breadcrumb: {
53 label: 'admin.system-settings', 54 label: 'admin.system-settings',
54 icon: 'settings' 55 icon: 'settings'
@@ -57,8 +58,13 @@ const routes: Routes = [ @@ -57,8 +58,13 @@ const routes: Routes = [
57 children: [ 58 children: [
58 { 59 {
59 path: '', 60 path: '',
60 - redirectTo: 'general',  
61 - pathMatch: 'full' 61 + data: {
  62 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  63 + redirectTo: {
  64 + SYS_ADMIN: '/settings/general',
  65 + TENANT_ADMIN: '/settings/home'
  66 + }
  67 + }
62 }, 68 },
63 { 69 {
64 path: 'general', 70 path: 'general',
@@ -127,6 +133,19 @@ const routes: Routes = [ @@ -127,6 +133,19 @@ const routes: Routes = [
127 resolve: { 133 resolve: {
128 loginProcessingUrl: OAuth2LoginProcessingUrlResolver 134 loginProcessingUrl: OAuth2LoginProcessingUrlResolver
129 } 135 }
  136 + },
  137 + {
  138 + path: 'home',
  139 + component: HomeSettingsComponent,
  140 + canDeactivate: [ConfirmOnExitGuard],
  141 + data: {
  142 + auth: [Authority.TENANT_ADMIN],
  143 + title: 'admin.home-settings',
  144 + breadcrumb: {
  145 + label: 'admin.home-settings',
  146 + icon: 'settings_applications'
  147 + }
  148 + }
130 } 149 }
131 ] 150 ]
132 } 151 }
@@ -26,6 +26,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m @@ -26,6 +26,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m
26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; 26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
27 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; 27 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
28 import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; 28 import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
  29 +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
29 30
30 @NgModule({ 31 @NgModule({
31 declarations: 32 declarations:
@@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial @@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial
35 SmsProviderComponent, 36 SmsProviderComponent,
36 SendTestSmsDialogComponent, 37 SendTestSmsDialogComponent,
37 SecuritySettingsComponent, 38 SecuritySettingsComponent,
38 - OAuth2SettingsComponent 39 + OAuth2SettingsComponent,
  40 + HomeSettingsComponent
39 ], 41 ],
40 imports: [ 42 imports: [
41 CommonModule, 43 CommonModule,
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.home-settings</span>
  23 + </div>
  24 + </mat-card-title>
  25 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  26 + </mat-progress-bar>
  27 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  28 + <mat-card-content style="padding-top: 16px;">
  29 + <form [formGroup]="homeSettings" (ngSubmit)="save()">
  30 + <fieldset [disabled]="isLoading$ | async">
  31 + <section class="tb-default-dashboard" fxFlex fxLayout="column">
  32 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  33 + <tb-dashboard-autocomplete
  34 + fxFlex
  35 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  36 + formControlName="dashboardId"
  37 + [dashboardsScope]="'tenant'"
  38 + [selectFirstDashboard]="false"
  39 + ></tb-dashboard-autocomplete>
  40 + <mat-checkbox fxFlex formControlName="hideDashboardToolbar">
  41 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  42 + </mat-checkbox>
  43 + </section>
  44 + </section>
  45 + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
  46 + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || homeSettings.invalid || !homeSettings.dirty"
  47 + type="submit">{{'action.save' | translate}}
  48 + </button>
  49 + </div>
  50 + </fieldset>
  51 + </form>
  52 + </mat-card-content>
  53 + </mat-card>
  54 +</div>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@import "../../../../../scss/constants";
  18 +
  19 +:host {
  20 + .tb-default-dashboard {
  21 + tb-dashboard-autocomplete {
  22 + @media #{$mat-gt-sm} {
  23 + padding-right: 12px;
  24 + }
  25 +
  26 + @media #{$mat-lt-md} {
  27 + padding-bottom: 12px;
  28 + }
  29 + }
  30 + mat-checkbox {
  31 + @media #{$mat-gt-sm} {
  32 + margin-top: 16px;
  33 + }
  34 + }
  35 + }
  36 +}
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { Router } from '@angular/router';
  22 +import { FormBuilder, FormGroup } from '@angular/forms';
  23 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  24 +import { DashboardService } from '@core/http/dashboard.service';
  25 +import { HomeDashboardInfo } from '@shared/models/dashboard.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
  27 +import { DashboardId } from '@shared/models/id/dashboard-id';
  28 +
  29 +@Component({
  30 + selector: 'tb-home-settings',
  31 + templateUrl: './home-settings.component.html',
  32 + styleUrls: ['./home-settings.component.scss', './settings-card.scss']
  33 +})
  34 +export class HomeSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
  35 +
  36 + homeSettings: FormGroup;
  37 +
  38 + constructor(protected store: Store<AppState>,
  39 + private router: Router,
  40 + private dashboardService: DashboardService,
  41 + public fb: FormBuilder) {
  42 + super(store);
  43 + }
  44 +
  45 + ngOnInit() {
  46 + this.homeSettings = this.fb.group({
  47 + dashboardId: [null],
  48 + hideDashboardToolbar: [true]
  49 + });
  50 + this.dashboardService.getTenantHomeDashboardInfo().subscribe(
  51 + (homeDashboardInfo) => {
  52 + this.setHomeDashboardInfo(homeDashboardInfo);
  53 + }
  54 + );
  55 + }
  56 +
  57 + save(): void {
  58 + const strDashboardId = this.homeSettings.get('dashboardId').value;
  59 + const dashboardId: DashboardId = strDashboardId ? new DashboardId(strDashboardId) : null;
  60 + const hideDashboardToolbar = this.homeSettings.get('hideDashboardToolbar').value;
  61 + const homeDashboardInfo: HomeDashboardInfo = {
  62 + dashboardId,
  63 + hideDashboardToolbar
  64 + };
  65 + this.dashboardService.setTenantHomeDashboardInfo(homeDashboardInfo).subscribe(
  66 + () => {
  67 + this.setHomeDashboardInfo(homeDashboardInfo);
  68 + }
  69 + );
  70 + }
  71 +
  72 + confirmForm(): FormGroup {
  73 + return this.homeSettings;
  74 + }
  75 +
  76 + private setHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo) {
  77 + this.homeSettings.reset({
  78 + dashboardId: homeDashboardInfo?.dashboardId?.id,
  79 + hideDashboardToolbar: isDefinedAndNotNull(homeDashboardInfo?.hideDashboardToolbar) ?
  80 + homeDashboardInfo?.hideDashboardToolbar : true
  81 + });
  82 + }
  83 +
  84 +}
@@ -72,6 +72,21 @@ @@ -72,6 +72,21 @@
72 <mat-label translate>customer.description</mat-label> 72 <mat-label translate>customer.description</mat-label>
73 <textarea matInput formControlName="description" rows="2"></textarea> 73 <textarea matInput formControlName="description" rows="2"></textarea>
74 </mat-form-field> 74 </mat-form-field>
  75 + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
  76 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  77 + <tb-dashboard-autocomplete
  78 + fxFlex
  79 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  80 + formControlName="homeDashboardId"
  81 + [dashboardsScope]="'customer'"
  82 + [customerId]="entity?.id.id"
  83 + [selectFirstDashboard]="false"
  84 + ></tb-dashboard-autocomplete>
  85 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  86 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  87 + </mat-checkbox>
  88 + </section>
  89 + </section>
75 </div> 90 </div>
76 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> 91 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
77 </fieldset> 92 </fieldset>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + .tb-default-dashboard {
  20 + tb-dashboard-autocomplete {
  21 + @media #{$mat-gt-sm} {
  22 + padding-right: 12px;
  23 + }
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding-bottom: 12px;
  27 + }
  28 + }
  29 + mat-checkbox {
  30 + @media #{$mat-gt-sm} {
  31 + margin-top: 16px;
  32 + }
  33 + }
  34 + }
  35 +}
@@ -23,10 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti @@ -23,10 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
23 import { TranslateService } from '@ngx-translate/core'; 23 import { TranslateService } from '@ngx-translate/core';
24 import { ContactBasedComponent } from '../../components/entity/contact-based.component'; 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 @Component({ 28 @Component({
28 selector: 'tb-customer', 29 selector: 'tb-customer',
29 - templateUrl: './customer.component.html' 30 + templateUrl: './customer.component.html',
  31 + styleUrls: ['./customer.component.scss']
30 }) 32 })
31 export class CustomerComponent extends ContactBasedComponent<Customer> { 33 export class CustomerComponent extends ContactBasedComponent<Customer> {
32 34
@@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { @@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
54 title: [entity ? entity.title : '', [Validators.required]], 56 title: [entity ? entity.title : '', [Validators.required]],
55 additionalInfo: this.fb.group( 57 additionalInfo: this.fb.group(
56 { 58 {
57 - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] 59 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  60 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  61 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  62 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
58 } 63 }
59 ) 64 )
60 } 65 }
@@ -65,6 +70,11 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { @@ -65,6 +70,11 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
65 this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic; 70 this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic;
66 this.entityForm.patchValue({title: entity.title}); 71 this.entityForm.patchValue({title: entity.title});
67 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); 72 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  73 + this.entityForm.patchValue({additionalInfo:
  74 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  75 + this.entityForm.patchValue({additionalInfo:
  76 + {homeDashboardHideToolbar: entity.additionalInfo &&
  77 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
68 } 78 }
69 79
70 onCustomerIdCopied(event) { 80 onCustomerIdCopied(event) {
@@ -14,11 +14,25 @@ @@ -14,11 +14,25 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { NgModule } from '@angular/core';  
18 -import { RouterModule, Routes } from '@angular/router'; 17 +import { Injectable, NgModule } from '@angular/core';
  18 +import { Resolve, RouterModule, Routes } from '@angular/router';
19 19
20 import { HomeLinksComponent } from './home-links.component'; 20 import { HomeLinksComponent } from './home-links.component';
21 import { Authority } from '@shared/models/authority.enum'; 21 import { Authority } from '@shared/models/authority.enum';
  22 +import { Observable } from 'rxjs';
  23 +import { HomeDashboard } from '@shared/models/dashboard.models';
  24 +import { DashboardService } from '@core/http/dashboard.service';
  25 +
  26 +@Injectable()
  27 +export class HomeDashboardResolver implements Resolve<HomeDashboard> {
  28 +
  29 + constructor(private dashboardService: DashboardService) {
  30 + }
  31 +
  32 + resolve(): Observable<HomeDashboard> {
  33 + return this.dashboardService.getHomeDashboard();
  34 + }
  35 +}
22 36
23 const routes: Routes = [ 37 const routes: Routes = [
24 { 38 {
@@ -31,12 +45,18 @@ const routes: Routes = [ @@ -31,12 +45,18 @@ const routes: Routes = [
31 label: 'home.home', 45 label: 'home.home',
32 icon: 'home' 46 icon: 'home'
33 } 47 }
  48 + },
  49 + resolve: {
  50 + homeDashboard: HomeDashboardResolver
34 } 51 }
35 } 52 }
36 ]; 53 ];
37 54
38 @NgModule({ 55 @NgModule({
39 imports: [RouterModule.forChild(routes)], 56 imports: [RouterModule.forChild(routes)],
40 - exports: [RouterModule] 57 + exports: [RouterModule],
  58 + providers: [
  59 + HomeDashboardResolver
  60 + ]
41 }) 61 })
42 export class HomeLinksRoutingModule { } 62 export class HomeLinksRoutingModule { }
@@ -15,23 +15,26 @@ @@ -15,23 +15,26 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">  
19 - <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">  
20 - <mat-card style="width: 100%;">  
21 - <mat-card-title>  
22 - <span translate class="mat-headline">{{section.name}}</span>  
23 - </mat-card-title>  
24 - <mat-card-content>  
25 - <mat-grid-list rowHeight="170px" [cols]="section.places.length">  
26 - <mat-grid-tile *ngFor="let place of section.places">  
27 - <a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">  
28 - <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>  
29 - <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>  
30 - <span translate>{{place.name}}</span>  
31 - </a>  
32 - </mat-grid-tile>  
33 - </mat-grid-list>  
34 - </mat-card-content>  
35 - </mat-card>  
36 - </mat-grid-tile>  
37 -</mat-grid-list> 18 +<tb-dashboard-page *ngIf="homeDashboard; else homeLinks" [embedded]="true" [dashboard]="homeDashboard" [hideToolbar]="homeDashboard.hideDashboardToolbar"></tb-dashboard-page>
  19 +<ng-template #homeLinks>
  20 + <mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">
  21 + <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">
  22 + <mat-card style="width: 100%;">
  23 + <mat-card-title>
  24 + <span translate class="mat-headline">{{section.name}}</span>
  25 + </mat-card-title>
  26 + <mat-card-content>
  27 + <mat-grid-list rowHeight="170px" [cols]="section.places.length">
  28 + <mat-grid-tile *ngFor="let place of section.places">
  29 + <a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
  30 + <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
  31 + <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
  32 + <span translate>{{place.name}}</span>
  33 + </a>
  34 + </mat-grid-tile>
  35 + </mat-grid-list>
  36 + </mat-card-content>
  37 + </mat-card>
  38 + </mat-grid-tile>
  39 + </mat-grid-list>
  40 +</ng-template>
@@ -15,6 +15,11 @@ @@ -15,6 +15,11 @@
15 */ 15 */
16 @import '../../../../../scss/constants'; 16 @import '../../../../../scss/constants';
17 17
  18 +:host {
  19 + width: 100%;
  20 + height: 100%;
  21 +}
  22 +
18 :host ::ng-deep { 23 :host ::ng-deep {
19 .tb-home-links { 24 .tb-home-links {
20 .mat-headline { 25 .mat-headline {
@@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service'; @@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service';
19 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; 19 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
20 import { MediaBreakpoints } from '@shared/models/constants'; 20 import { MediaBreakpoints } from '@shared/models/constants';
21 import { HomeSection } from '@core/services/menu.models'; 21 import { HomeSection } from '@core/services/menu.models';
  22 +import { ActivatedRoute } from '@angular/router';
  23 +import { HomeDashboard } from '@shared/models/dashboard.models';
22 24
23 @Component({ 25 @Component({
24 selector: 'tb-home-links', 26 selector: 'tb-home-links',
@@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit { @@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit {
31 33
32 cols = 2; 34 cols = 2;
33 35
  36 + homeDashboard: HomeDashboard = this.route.snapshot.data.homeDashboard;
  37 +
34 constructor(private menuService: MenuService, 38 constructor(private menuService: MenuService,
35 - public breakpointObserver: BreakpointObserver) { 39 + public breakpointObserver: BreakpointObserver,
  40 + private route: ActivatedRoute) {
36 } 41 }
37 42
38 ngOnInit() { 43 ngOnInit() {
39 - this.updateColumnCount();  
40 - this.breakpointObserver  
41 - .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])  
42 - .subscribe((state: BreakpointState) => this.updateColumnCount()); 44 + if (!this.homeDashboard) {
  45 + this.updateColumnCount();
  46 + this.breakpointObserver
  47 + .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])
  48 + .subscribe((state: BreakpointState) => this.updateColumnCount());
  49 + }
43 } 50 }
44 51
45 private updateColumnCount() { 52 private updateColumnCount() {
@@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common'; @@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common';
20 import { HomeLinksRoutingModule } from './home-links-routing.module'; 20 import { HomeLinksRoutingModule } from './home-links-routing.module';
21 import { HomeLinksComponent } from './home-links.component'; 21 import { HomeLinksComponent } from './home-links.component';
22 import { SharedModule } from '@app/shared/shared.module'; 22 import { SharedModule } from '@app/shared/shared.module';
  23 +import { HomeComponentsModule } from '@home/components/home-components.module';
23 24
24 @NgModule({ 25 @NgModule({
25 declarations: 26 declarations:
@@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module'; @@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module';
29 imports: [ 30 imports: [
30 CommonModule, 31 CommonModule,
31 SharedModule, 32 SharedModule,
  33 + HomeComponentsModule,
32 HomeLinksRoutingModule 34 HomeLinksRoutingModule
33 ] 35 ]
34 }) 36 })
@@ -63,6 +63,20 @@ @@ -63,6 +63,20 @@
63 </mat-option> 63 </mat-option>
64 </mat-select> 64 </mat-select>
65 </mat-form-field> 65 </mat-form-field>
  66 + <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row">
  67 + <tb-dashboard-autocomplete
  68 + fxFlex
  69 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  70 + formControlName="homeDashboardId"
  71 + [dashboardsScope]="user?.authority === authorities.TENANT_ADMIN ? 'tenant' : 'customer'"
  72 + [tenantId]="user?.tenantId?.id"
  73 + [customerId]="user?.customerId?.id"
  74 + [selectFirstDashboard]="false"
  75 + ></tb-dashboard-autocomplete>
  76 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  77 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  78 + </mat-checkbox>
  79 + </section>
66 <div fxLayout="row" style="padding-bottom: 16px;"> 80 <div fxLayout="row" style="padding-bottom: 16px;">
67 <button mat-button mat-raised-button color="primary" 81 <button mat-button mat-raised-button color="primary"
68 type="button" 82 type="button"
@@ -38,5 +38,21 @@ @@ -38,5 +38,21 @@
38 font-size: 16px; 38 font-size: 16px;
39 font-weight: 400; 39 font-weight: 400;
40 } 40 }
  41 + .tb-home-dashboard {
  42 + tb-dashboard-autocomplete {
  43 + @media #{$mat-gt-sm} {
  44 + padding-right: 12px;
  45 + }
  46 +
  47 + @media #{$mat-lt-md} {
  48 + padding-bottom: 12px;
  49 + }
  50 + }
  51 + mat-checkbox {
  52 + @media #{$mat-gt-sm} {
  53 + margin-top: 16px;
  54 + }
  55 + }
  56 + }
41 } 57 }
42 } 58 }
@@ -32,6 +32,7 @@ import { MatDialog } from '@angular/material/dialog'; @@ -32,6 +32,7 @@ import { MatDialog } from '@angular/material/dialog';
32 import { DialogService } from '@core/services/dialog.service'; 32 import { DialogService } from '@core/services/dialog.service';
33 import { AuthService } from '@core/auth/auth.service'; 33 import { AuthService } from '@core/auth/auth.service';
34 import { ActivatedRoute } from '@angular/router'; 34 import { ActivatedRoute } from '@angular/router';
  35 +import { isDefinedAndNotNull } from '@core/utils';
35 36
36 @Component({ 37 @Component({
37 selector: 'tb-profile', 38 selector: 'tb-profile',
@@ -66,7 +67,9 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -66,7 +67,9 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
66 email: ['', [Validators.required, Validators.email]], 67 email: ['', [Validators.required, Validators.email]],
67 firstName: [''], 68 firstName: [''],
68 lastName: [''], 69 lastName: [''],
69 - language: [''] 70 + language: [''],
  71 + homeDashboardId: [null],
  72 + homeDashboardHideToolbar: [true]
70 }); 73 });
71 } 74 }
72 75
@@ -76,6 +79,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -76,6 +79,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
76 this.user.additionalInfo = {}; 79 this.user.additionalInfo = {};
77 } 80 }
78 this.user.additionalInfo.lang = this.profile.get('language').value; 81 this.user.additionalInfo.lang = this.profile.get('language').value;
  82 + this.user.additionalInfo.homeDashboardId = this.profile.get('homeDashboardId').value;
  83 + this.user.additionalInfo.homeDashboardHideToolbar = this.profile.get('homeDashboardHideToolbar').value;
79 this.userService.saveUser(this.user).subscribe( 84 this.userService.saveUser(this.user).subscribe(
80 (user) => { 85 (user) => {
81 this.userLoaded(user); 86 this.userLoaded(user);
@@ -106,12 +111,23 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -106,12 +111,23 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
106 this.user = user; 111 this.user = user;
107 this.profile.reset(user); 112 this.profile.reset(user);
108 let lang; 113 let lang;
109 - if (user.additionalInfo && user.additionalInfo.lang) {  
110 - lang = user.additionalInfo.lang;  
111 - } else { 114 + let homeDashboardId;
  115 + let homeDashboardHideToolbar = true;
  116 + if (user.additionalInfo) {
  117 + if (user.additionalInfo.lang) {
  118 + lang = user.additionalInfo.lang;
  119 + }
  120 + homeDashboardId = user.additionalInfo.homeDashboardId;
  121 + if (isDefinedAndNotNull(user.additionalInfo.homeDashboardHideToolbar)) {
  122 + homeDashboardHideToolbar = user.additionalInfo.homeDashboardHideToolbar;
  123 + }
  124 + }
  125 + if (!lang) {
112 lang = this.translate.currentLang; 126 lang = this.translate.currentLang;
113 } 127 }
114 this.profile.get('language').setValue(lang); 128 this.profile.get('language').setValue(lang);
  129 + this.profile.get('homeDashboardId').setValue(homeDashboardId);
  130 + this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar);
115 } 131 }
116 132
117 confirmForm(): FormGroup { 133 confirmForm(): FormGroup {
@@ -60,6 +60,21 @@ @@ -60,6 +60,21 @@
60 <mat-label translate>tenant.description</mat-label> 60 <mat-label translate>tenant.description</mat-label>
61 <textarea matInput formControlName="description" rows="2"></textarea> 61 <textarea matInput formControlName="description" rows="2"></textarea>
62 </mat-form-field> 62 </mat-form-field>
  63 + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
  64 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  65 + <tb-dashboard-autocomplete
  66 + fxFlex
  67 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  68 + formControlName="homeDashboardId"
  69 + [dashboardsScope]="'tenant'"
  70 + [tenantId]="entity?.id.id"
  71 + [selectFirstDashboard]="false"
  72 + ></tb-dashboard-autocomplete>
  73 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  74 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  75 + </mat-checkbox>
  76 + </section>
  77 + </section>
63 </div> 78 </div>
64 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> 79 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
65 </fieldset> 80 </fieldset>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + .tb-default-dashboard {
  20 + tb-dashboard-autocomplete {
  21 + @media #{$mat-gt-sm} {
  22 + padding-right: 12px;
  23 + }
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding-bottom: 12px;
  27 + }
  28 + }
  29 + mat-checkbox {
  30 + @media #{$mat-gt-sm} {
  31 + margin-top: 16px;
  32 + }
  33 + }
  34 + }
  35 +}
@@ -23,11 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti @@ -23,11 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
23 import { TranslateService } from '@ngx-translate/core'; 23 import { TranslateService } from '@ngx-translate/core';
24 import { ContactBasedComponent } from '../../components/entity/contact-based.component'; 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 @Component({ 28 @Component({
28 selector: 'tb-tenant', 29 selector: 'tb-tenant',
29 templateUrl: './tenant.component.html', 30 templateUrl: './tenant.component.html',
30 - styleUrls: [] 31 + styleUrls: ['./tenant.component.scss']
31 }) 32 })
32 export class TenantComponent extends ContactBasedComponent<TenantInfo> { 33 export class TenantComponent extends ContactBasedComponent<TenantInfo> {
33 34
@@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { @@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
54 tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]], 55 tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]],
55 additionalInfo: this.fb.group( 56 additionalInfo: this.fb.group(
56 { 57 {
57 - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] 58 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  59 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  60 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  61 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
58 } 62 }
59 ) 63 )
60 } 64 }
@@ -65,6 +69,11 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { @@ -65,6 +69,11 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
65 this.entityForm.patchValue({title: entity.title}); 69 this.entityForm.patchValue({title: entity.title});
66 this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId}); 70 this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId});
67 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); 71 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  72 + this.entityForm.patchValue({additionalInfo:
  73 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  74 + this.entityForm.patchValue({additionalInfo:
  75 + {homeDashboardHideToolbar: entity.additionalInfo &&
  76 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
68 } 77 }
69 78
70 updateFormState() { 79 updateFormState() {
@@ -95,6 +95,20 @@ @@ -95,6 +95,20 @@
95 {{ 'user.always-fullscreen' | translate }} 95 {{ 'user.always-fullscreen' | translate }}
96 </mat-checkbox> 96 </mat-checkbox>
97 </section> 97 </section>
  98 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  99 + <tb-dashboard-autocomplete
  100 + fxFlex
  101 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  102 + formControlName="homeDashboardId"
  103 + [dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
  104 + [tenantId]="entity?.tenantId?.id"
  105 + [customerId]="entity?.customerId?.id"
  106 + [selectFirstDashboard]="false"
  107 + ></tb-dashboard-autocomplete>
  108 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  109 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  110 + </mat-checkbox>
  111 + </section>
98 </section> 112 </section>
99 </div> 113 </div>
100 </fieldset> 114 </fieldset>
@@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model'; @@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model';
23 import { selectAuth } from '@core/auth/auth.selectors'; 23 import { selectAuth } from '@core/auth/auth.selectors';
24 import { map } from 'rxjs/operators'; 24 import { map } from 'rxjs/operators';
25 import { Authority } from '@shared/models/authority.enum'; 25 import { Authority } from '@shared/models/authority.enum';
26 -import { isUndefined } from '@core/utils'; 26 +import { isDefinedAndNotNull, isUndefined } from '@core/utils';
27 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; 27 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
28 28
29 @Component({ 29 @Component({
@@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent<User> { @@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent<User> {
74 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], 74 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
75 defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], 75 defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
76 defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], 76 defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
  77 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  78 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  79 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
77 } 80 }
78 ) 81 )
79 } 82 }
@@ -89,6 +92,11 @@ export class UserComponent extends EntityComponent<User> { @@ -89,6 +92,11 @@ export class UserComponent extends EntityComponent<User> {
89 {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); 92 {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
90 this.entityForm.patchValue({additionalInfo: 93 this.entityForm.patchValue({additionalInfo:
91 {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}}); 94 {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}});
  95 + this.entityForm.patchValue({additionalInfo:
  96 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  97 + this.entityForm.patchValue({additionalInfo:
  98 + {homeDashboardHideToolbar: entity.additionalInfo &&
  99 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
92 } 100 }
93 101
94 } 102 }
@@ -35,6 +35,7 @@ @@ -35,6 +35,7 @@
35 [isEditActionEnabled]="true" 35 [isEditActionEnabled]="true"
36 [isExportActionEnabled]="true" 36 [isExportActionEnabled]="true"
37 [isRemoveActionEnabled]="!isReadOnly" 37 [isRemoveActionEnabled]="!isReadOnly"
  38 + [disableWidgetInteraction]="true"
38 [callbacks]="dashboardCallbacks"></tb-dashboard> 39 [callbacks]="dashboardCallbacks"></tb-dashboard>
39 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons"> 40 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
40 </tb-footer-fab-buttons> 41 </tb-footer-fab-buttons>
@@ -14,7 +14,17 @@ @@ -14,7 +14,17 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 17 +import {
  18 + Component,
  19 + forwardRef,
  20 + Inject,
  21 + Injector,
  22 + Input,
  23 + OnInit,
  24 + StaticProvider,
  25 + ViewChild,
  26 + ViewContainerRef
  27 +} from '@angular/core';
18 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 28 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 import { Observable, of } from 'rxjs'; 29 import { Observable, of } from 'rxjs';
20 import { PageLink } from '@shared/models/page/page-link'; 30 import { PageLink } from '@shared/models/page/page-link';
@@ -32,7 +42,7 @@ import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef @@ -32,7 +42,7 @@ import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef
32 import { BreakpointObserver } from '@angular/cdk/layout'; 42 import { BreakpointObserver } from '@angular/cdk/layout';
33 import { DOCUMENT } from '@angular/common'; 43 import { DOCUMENT } from '@angular/common';
34 import { WINDOW } from '@core/services/window.service'; 44 import { WINDOW } from '@core/services/window.service';
35 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 45 +import { ComponentPortal } from '@angular/cdk/portal';
36 import { 46 import {
37 DASHBOARD_SELECT_PANEL_DATA, 47 DASHBOARD_SELECT_PANEL_DATA,
38 DashboardSelectPanelComponent, 48 DashboardSelectPanelComponent,
@@ -186,12 +196,12 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit { @@ -186,12 +196,12 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
186 overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector)); 196 overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
187 } 197 }
188 198
189 - private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector {  
190 - const injectionTokens = new WeakMap<any, any>([  
191 - [DASHBOARD_SELECT_PANEL_DATA, data],  
192 - [OverlayRef, overlayRef]  
193 - ]);  
194 - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); 199 + private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): Injector {
  200 + const providers: StaticProvider[] = [
  201 + {provide: DASHBOARD_SELECT_PANEL_DATA, useValue: data},
  202 + {provide: OverlayRef, useValue: overlayRef}
  203 + ];
  204 + return Injector.create({parent: this.viewContainerRef.injector, providers});
195 } 205 }
196 206
197 private updateView() { 207 private updateView() {
@@ -28,12 +28,17 @@ class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFiel @@ -28,12 +28,17 @@ class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFiel
28 ); 28 );
29 }); 29 });
30 30
  31 + let row = false;
  32 + if (this.props.form.direction === 'row') {
  33 + row = true;
  34 + }
  35 +
31 return ( 36 return (
32 <FormControl component='fieldset' 37 <FormControl component='fieldset'
33 className={this.props.form.htmlClass} 38 className={this.props.form.htmlClass}
34 disabled={this.props.form.readonly}> 39 disabled={this.props.form.readonly}>
35 <FormLabel component='legend'>{this.props.form.title}</FormLabel> 40 <FormLabel component='legend'>{this.props.form.title}</FormLabel>
36 - <RadioGroup name={this.props.form.title} value={this.props.value} onChange={(e) => { 41 + <RadioGroup row={row} name={this.props.form.title} value={this.props.value} onChange={(e) => {
37 this.props.onChangeValidate(e); 42 this.props.onChangeValidate(e);
38 }}> 43 }}>
39 {items} 44 {items}
@@ -22,6 +22,7 @@ import { @@ -22,6 +22,7 @@ import {
22 KeyLabelItem 22 KeyLabelItem
23 } from '@shared/components/json-form/react/json-form.models'; 23 } from '@shared/components/json-form/react/json-form.models';
24 import { Mode } from 'rc-select/lib/interface'; 24 import { Mode } from 'rc-select/lib/interface';
  25 +import { deepClone } from '@core/utils';
25 26
26 interface ThingsboardRcSelectState extends JsonFormFieldState { 27 interface ThingsboardRcSelectState extends JsonFormFieldState {
27 currentValue: KeyLabelItem | KeyLabelItem[]; 28 currentValue: KeyLabelItem | KeyLabelItem[];
@@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar @@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
151 labelClass += ' tb-focused'; 152 labelClass += ' tb-focused';
152 } 153 }
153 let mode: Mode; 154 let mode: Mode;
154 - if (this.props.form.tags) {  
155 - mode = 'tags';  
156 - } else if (this.props.form.multiple) {  
157 - mode = 'multiple'; 155 + let value = this.state.currentValue;
  156 + if (this.props.form.tags || this.props.form.multiple) {
  157 + value = deepClone(value);
  158 + if (this.props.form.tags) {
  159 + mode = 'tags';
  160 + } else if (this.props.form.multiple) {
  161 + mode = 'multiple';
  162 + }
158 } 163 }
159 164
160 const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}}; 165 const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}};
@@ -176,12 +181,13 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar @@ -176,12 +181,13 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
176 maxTagTextLength={this.props.form.maxTagTextLength} 181 maxTagTextLength={this.props.form.maxTagTextLength}
177 disabled={this.props.form.readonly} 182 disabled={this.props.form.readonly}
178 optionLabelProp='children' 183 optionLabelProp='children'
179 - value={this.state.currentValue} 184 + value={value}
180 labelInValue={true} 185 labelInValue={true}
181 onSelect={this.onSelect} 186 onSelect={this.onSelect}
182 onDeselect={this.onDeselect} 187 onDeselect={this.onDeselect}
183 onFocus={this.onFocus} 188 onFocus={this.onFocus}
184 onBlur={this.onBlur} 189 onBlur={this.onBlur}
  190 + placeholder={this.props.form.placeholder}
185 style={this.props.form.style || {width: '100%'}}> 191 style={this.props.form.style || {width: '100%'}}>
186 {options} 192 {options}
187 </Select> 193 </Select>
@@ -14,7 +14,18 @@ @@ -14,7 +14,18 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 17 +import {
  18 + Component,
  19 + forwardRef,
  20 + Inject,
  21 + Injector,
  22 + Input,
  23 + OnDestroy,
  24 + OnInit,
  25 + StaticProvider,
  26 + ViewChild,
  27 + ViewContainerRef
  28 +} from '@angular/core';
18 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 29 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 import { TranslateService } from '@ngx-translate/core'; 30 import { TranslateService } from '@ngx-translate/core';
20 import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; 31 import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
@@ -32,7 +43,7 @@ import { @@ -32,7 +43,7 @@ import {
32 TimewindowPanelComponent, 43 TimewindowPanelComponent,
33 TimewindowPanelData 44 TimewindowPanelData
34 } from '@shared/components/time/timewindow-panel.component'; 45 } from '@shared/components/time/timewindow-panel.component';
35 -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 46 +import { ComponentPortal } from '@angular/cdk/portal';
36 import { MediaBreakpoints } from '@shared/models/constants'; 47 import { MediaBreakpoints } from '@shared/models/constants';
37 import { BreakpointObserver } from '@angular/cdk/layout'; 48 import { BreakpointObserver } from '@angular/cdk/layout';
38 import { WINDOW } from '@core/services/window.service'; 49 import { WINDOW } from '@core/services/window.service';
@@ -229,12 +240,12 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces @@ -229,12 +240,12 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
229 }); 240 });
230 } 241 }
231 242
232 - private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector {  
233 - const injectionTokens = new WeakMap<any, any>([  
234 - [TIMEWINDOW_PANEL_DATA, data],  
235 - [OverlayRef, overlayRef]  
236 - ]);  
237 - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); 243 + private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): Injector {
  244 + const providers: StaticProvider[] = [
  245 + {provide: TIMEWINDOW_PANEL_DATA, useValue: data},
  246 + {provide: OverlayRef, useValue: overlayRef}
  247 + ];
  248 + return Injector.create({parent: this.viewContainerRef.injector, providers});
238 } 249 }
239 250
240 registerOnChange(fn: any): void { 251 registerOnChange(fn: any): void {
@@ -20,9 +20,11 @@ import { @@ -20,9 +20,11 @@ import {
20 Directive, 20 Directive,
21 ElementRef, HostBinding, 21 ElementRef, HostBinding,
22 Inject, 22 Inject,
  23 + Injector,
23 Input, 24 Input,
24 NgZone, 25 NgZone,
25 OnDestroy, Optional, 26 OnDestroy, Optional,
  27 + StaticProvider,
26 ViewChild, 28 ViewChild,
27 ViewContainerRef 29 ViewContainerRef
28 } from '@angular/core'; 30 } from '@angular/core';
@@ -34,7 +36,6 @@ import { BreakpointObserver } from '@angular/cdk/layout'; @@ -34,7 +36,6 @@ import { BreakpointObserver } from '@angular/cdk/layout';
34 import { MediaBreakpoints } from '@shared/models/constants'; 36 import { MediaBreakpoints } from '@shared/models/constants';
35 import { MatButton } from '@angular/material/button'; 37 import { MatButton } from '@angular/material/button';
36 import Timeout = NodeJS.Timeout; 38 import Timeout = NodeJS.Timeout;
37 -import { PortalInjector } from '@angular/cdk/portal';  
38 39
39 @Directive({ 40 @Directive({
40 selector: '[tb-toast]' 41 selector: '[tb-toast]'
@@ -138,10 +139,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy { @@ -138,10 +139,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
138 this.toastComponentRef.destroy(); 139 this.toastComponentRef.destroy();
139 } 140 }
140 }; 141 };
141 - const injectionTokens = new WeakMap<any, any>([  
142 - [MAT_SNACK_BAR_DATA, data]  
143 - ]);  
144 - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); 142 + const providers: StaticProvider[] = [
  143 + {provide: MAT_SNACK_BAR_DATA, useValue: data}
  144 + ];
  145 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
145 this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector); 146 this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector);
146 this.cd.detectChanges(); 147 this.cd.detectChanges();
147 148
@@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo { @@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo {
106 configuration?: DashboardConfiguration; 106 configuration?: DashboardConfiguration;
107 } 107 }
108 108
  109 +export interface HomeDashboard extends Dashboard {
  110 + hideDashboardToolbar: boolean;
  111 +}
  112 +
  113 +export interface HomeDashboardInfo {
  114 + dashboardId: DashboardId;
  115 + hideDashboardToolbar: boolean;
  116 +}
  117 +
109 export function isPublicDashboard(dashboard: DashboardInfo): boolean { 118 export function isPublicDashboard(dashboard: DashboardInfo): boolean {
110 if (dashboard && dashboard.assignedCustomers) { 119 if (dashboard && dashboard.assignedCustomers) {
111 return dashboard.assignedCustomers 120 return dashboard.assignedCustomers
@@ -54,7 +54,10 @@ @@ -54,7 +54,10 @@
54 "share-via": "Sdílet přes {{provider}}", 54 "share-via": "Sdílet přes {{provider}}",
55 "continue": "Pokračovat", 55 "continue": "Pokračovat",
56 "discard-changes": "Zahodit změny", 56 "discard-changes": "Zahodit změny",
57 - "download": "Stáhnout" 57 + "download": "Stáhnout",
  58 + "next-with-label": "Další: {{label}}",
  59 + "read-more": "Zobrazit více",
  60 + "hide": "Skrýt"
58 }, 61 },
59 "aggregation": { 62 "aggregation": {
60 "aggregation": "Agregace", 63 "aggregation": "Agregace",
@@ -77,6 +80,8 @@ @@ -77,6 +80,8 @@
77 "test-mail-sent": "Testovací zpráva byla úspěšně odeslána!", 80 "test-mail-sent": "Testovací zpráva byla úspěšně odeslána!",
78 "base-url": "Základní URL", 81 "base-url": "Základní URL",
79 "base-url-required": "Hodnota Základní URL je povinná.", 82 "base-url-required": "Hodnota Základní URL je povinná.",
  83 + "prohibit-different-url": "Zakázat použití názvu hosta z hlaviček požadavku klienta",
  84 + "prohibit-different-url-hint": "Toto nastavení by mělo být povoleno v produkčních prostředích. Pokud je zakázáno, může způsobit bezpečnostní problémy",
80 "mail-from": "Email od", 85 "mail-from": "Email od",
81 "mail-from-required": "Hodnota Email od je povinná.", 86 "mail-from-required": "Hodnota Email od je povinná.",
82 "smtp-protocol": "SMTP protokol", 87 "smtp-protocol": "SMTP protokol",
@@ -99,6 +104,33 @@ @@ -99,6 +104,33 @@
99 "proxy-user": "Uživatel proxy", 104 "proxy-user": "Uživatel proxy",
100 "proxy-password": "Heslo proxy", 105 "proxy-password": "Heslo proxy",
101 "send-test-mail": "Odeslat testovací zprávu", 106 "send-test-mail": "Odeslat testovací zprávu",
  107 + "sms-provider": "Poskytovatel SMS",
  108 + "sms-provider-settings": "Nastavení poskytovatele SMS",
  109 + "sms-provider-type": "Typ poskytovatele SMS",
  110 + "sms-provider-type-required": "Typ poskytovatele SMS je povinný.",
  111 + "sms-provider-type-aws-sns": "Amazon SNS",
  112 + "sms-provider-type-twilio": "Twilio",
  113 + "aws-access-key-id": "AWS Access Key ID",
  114 + "aws-access-key-id-required": "AWS Access Key ID je povinný",
  115 + "aws-secret-access-key": "AWS Secret Access Key",
  116 + "aws-secret-access-key-required": "AWS Secret Access Key je povinný",
  117 + "aws-region": "AWS Region",
  118 + "aws-region-required": "AWS Region je povinný",
  119 + "number-from": "Telefonní číslo odesílatele",
  120 + "number-from-required": "Telefonní číslo Odesílatele je povinné.",
  121 + "number-to": "Telefonní číslo příjemce",
  122 + "number-to-required": "Telefonní číslo příjemce je povinné.",
  123 + "phone-number-hint": "Telefonní číslo ve formátu E.164, např. +19995550123",
  124 + "phone-number-pattern": "Neplatné telefonní číslo. Mělo by odpovídat formátu E.164, např. +19995550123.",
  125 + "sms-message": "SMS zpráva",
  126 + "sms-message-required": "SMS zpráva je povinná.",
  127 + "sms-message-max-length": "SMS zpráva nemůže být delší než 1600 znaků",
  128 + "twilio-account-sid": "Twilio Account SID",
  129 + "twilio-account-sid-required": "Twilio Account SID je povinné",
  130 + "twilio-account-token": "Twilio Account Token",
  131 + "twilio-account-token-required": "Twilio Account Token je povinný",
  132 + "send-test-sms": "Odeslat testovací SMS",
  133 + "test-sms-sent": "Testovací SMS úspěšně odeslána!",
102 "security-settings": "Bezpečnostní nastavení", 134 "security-settings": "Bezpečnostní nastavení",
103 "password-policy": "Politika hesel", 135 "password-policy": "Politika hesel",
104 "minimum-password-length": "Minimální délka hesla", 136 "minimum-password-length": "Minimální délka hesla",
@@ -119,8 +151,74 @@ @@ -119,8 +151,74 @@
119 "general-policy": "Obecná politika", 151 "general-policy": "Obecná politika",
120 "max-failed-login-attempts": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu", 152 "max-failed-login-attempts": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu",
121 "minimum-max-failed-login-attempts-range": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu nemůže být záporný", 153 "minimum-max-failed-login-attempts-range": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu nemůže být záporný",
122 - "user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email"  
123 - }, 154 + "user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email",
  155 + "domain-name": "Doménové jméno",
  156 + "domain-name-unique": "Doménové jméno a protokol musí být unikátní.",
  157 + "error-verification-url": "Doménové jméno by nemělo obsahovat symbol '/' ani ':'. Příklad: thingsboard.io",
  158 + "oauth2": {
  159 + "access-token-uri": "URI přístupového tokenu",
  160 + "access-token-uri-required": "URI přístupového tokenu je povinné.",
  161 + "activate-user": "Aktivovat uživatele",
  162 + "add-domain": "Přidat doménu",
  163 + "delete-domain": "Smazat doménu",
  164 + "add-provider": "Přidat poskytovatele",
  165 + "delete-provider": "Smazat poskytovatele",
  166 + "allow-user-creation": "Povolit vytvoření uživatele",
  167 + "always-fullscreen": "Vždy v režimu celé obrazovky",
  168 + "authorization-uri": "Autorizační URI",
  169 + "authorization-uri-required": "Autorizační URI je povinné.",
  170 + "client-authentication-method": "Metoda autentizace klienta",
  171 + "client-id": "ID klienta",
  172 + "client-id-required": "ID klienta je povinné.",
  173 + "client-secret": "Heslo klienta",
  174 + "client-secret-required": "Heslo klienta je povinné.",
  175 + "custom-setting": "Vlastní nastavení",
  176 + "customer-name-pattern": "Vzor názvu zákazníka",
  177 + "default-dashboard-name": "Název defaultního dashboardu",
  178 + "delete-domain-text": "Budťe opatrní, protože po potvrzení nebudou doména ani žádná data poskytovatele dostupné.",
  179 + "delete-domain-title": "Jste si jisti, že chcete smazat nastavení domény '{{domainName}}'?",
  180 + "delete-registration-text": "Buďte opatrní, protože po potvrzení nebudou data poskytovatele dostupná.",
  181 + "delete-registration-title": "Jste si jisti, že chcete smazat poskytovatele '{{name}}'?",
  182 + "email-attribute-key": "Atribut klíče email",
  183 + "email-attribute-key-required": "Atribut klíče email je povinný.",
  184 + "first-name-attribute-key": "Atribut klíče jméno",
  185 + "general": "Obecné",
  186 + "jwk-set-uri": "JSON Web Key URI",
  187 + "last-name-attribute-key": "Atribut klíče příjmení",
  188 + "login-button-icon": "Ikona tlačítka přihlášení",
  189 + "login-button-label": "Označení poskytovatele",
  190 + "login-button-label-placeholder": "Přihlásit se přes $(Provider label)",
  191 + "login-button-label-required": "Označení je povinné.",
  192 + "login-provider": "Poskytovatel přihlášení",
  193 + "mapper": "Mapper",
  194 + "new-domain": "Nová doména",
  195 + "oauth2": "OAuth2",
  196 + "redirect-uri-template": "Šablona URI přesměrování",
  197 + "copy-redirect-uri": "Zkopírovat URI přesměrování",
  198 + "registration-id": "ID registrace",
  199 + "registration-id-required": "ID registrace je povinné.",
  200 + "registration-id-unique": "Id registrace musí být v systému unikátní.",
  201 + "scope": "Rozsah",
  202 + "scope-required": "Rozsah je povinný.",
  203 + "tenant-name-pattern": "Vzor názvu tenanta",
  204 + "tenant-name-pattern-required": "Vzor názvu tenanta je povinný.",
  205 + "tenant-name-strategy": "Strategie názvu tenanta",
  206 + "type": "Typ mapperu",
  207 + "uri-pattern-error": "Neplatný formát URI.",
  208 + "url": "URL",
  209 + "url-pattern": "Neplatný formát URL.",
  210 + "url-required": "URL je povinná.",
  211 + "user-info-uri": "User info URI",
  212 + "user-info-uri-required": "User info URI je povinné.",
  213 + "user-name-attribute-name": "Atribut klíče název uživatele",
  214 + "user-name-attribute-name-required": "Atribut klíče název uživatele je povinný",
  215 + "protocol": "Protokol",
  216 + "domain-schema-http": "HTTP",
  217 + "domain-schema-https": "HTTPS",
  218 + "domain-schema-mixed": "HTTP+HTTPS",
  219 + "enable": "Povolit nastavení OAuth2"
  220 + }
  221 + },
124 "alarm": { 222 "alarm": {
125 "alarm": "Alarm", 223 "alarm": "Alarm",
126 "alarms": "Alarmy", 224 "alarms": "Alarmy",
@@ -128,6 +226,8 @@ @@ -128,6 +226,8 @@
128 "no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.", 226 "no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.",
129 "alarm-required": "Alarm je povinný", 227 "alarm-required": "Alarm je povinný",
130 "alarm-status": "Stav alarmu", 228 "alarm-status": "Stav alarmu",
  229 + "alarm-status-list": "Seznam stavů alarmu",
  230 + "any-status": "Všechny stavy",
131 "search-status": { 231 "search-status": {
132 "ANY": "Všechny", 232 "ANY": "Všechny",
133 "ACTIVE": "Aktivní", 233 "ACTIVE": "Aktivní",
@@ -154,6 +254,8 @@ @@ -154,6 +254,8 @@
154 "end-time": "Datum ukončení", 254 "end-time": "Datum ukončení",
155 "ack-time": "Datum přijetí", 255 "ack-time": "Datum přijetí",
156 "clear-time": "Datum vyřešení", 256 "clear-time": "Datum vyřešení",
  257 + "alarm-severity-list": "Seznam závažností alarmu",
  258 + "any-severity": "Všechny závažnosti",
157 "severity-critical": "Kritická", 259 "severity-critical": "Kritická",
158 "severity-major": "Vysoká", 260 "severity-major": "Vysoká",
159 "severity-minor": "Nízká", 261 "severity-minor": "Nízká",
@@ -176,12 +278,16 @@ @@ -176,12 +278,16 @@
176 "clear-alarm-title": "Odstranit alarm", 278 "clear-alarm-title": "Odstranit alarm",
177 "clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?", 279 "clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?",
178 "alarm-status-filter": "Filtr stavu alarmu", 280 "alarm-status-filter": "Filtr stavu alarmu",
  281 + "alarm-filter": "Filtr alarmu",
179 "max-count-load": "Maximální počet nahraných alarmů (0 - neomezeně)", 282 "max-count-load": "Maximální počet nahraných alarmů (0 - neomezeně)",
180 "max-count-load-required": "Maximální počet nahraných alarmů je povinný.", 283 "max-count-load-required": "Maximální počet nahraných alarmů je povinný.",
181 "max-count-load-error-min": "Minimální hodnota je 0.", 284 "max-count-load-error-min": "Minimální hodnota je 0.",
182 "fetch-size": "Velikost dávky", 285 "fetch-size": "Velikost dávky",
183 "fetch-size-required": "Velikost dávky je povinná.", 286 "fetch-size-required": "Velikost dávky je povinná.",
184 - "fetch-size-error-min": "Minimální hodnota je 10." 287 + "fetch-size-error-min": "Minimální hodnota je 10.",
  288 + "alarm-type-list": "Seznam typů alarmu",
  289 + "any-type": "Všechny typy",
  290 + "search-propagated-alarms": "Vyhledat zpropagované alarmy"
185 }, 291 },
186 "alias": { 292 "alias": {
187 "add": "Přidat alias", 293 "add": "Přidat alias",
@@ -211,6 +317,7 @@ @@ -211,6 +317,7 @@
211 "filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", 317 "filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
212 "filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity", 318 "filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity",
213 "filter-type-entity-view-search-query-description": "Entitní pohledy typů {{entityViewTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", 319 "filter-type-entity-view-search-query-description": "Entitní pohledy typů {{entityViewTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
  320 + "filter-type-apiUsageState": "Stav využití Api",
214 "entity-filter": "Filtr entity", 321 "entity-filter": "Filtr entity",
215 "resolve-multiple": "Použít jako více entit", 322 "resolve-multiple": "Použít jako více entit",
216 "filter-type": "Typ filtru", 323 "filter-type": "Typ filtru",
@@ -325,6 +432,59 @@ @@ -325,6 +432,59 @@
325 "no-attributes-text": "Žádné atributy nebyly nalezeny", 432 "no-attributes-text": "Žádné atributy nebyly nalezeny",
326 "no-telemetry-text": "Žádná telemetrie nebyla nalezena" 433 "no-telemetry-text": "Žádná telemetrie nebyla nalezena"
327 }, 434 },
  435 + "api-usage": {
  436 + "api-usage": "Využití Api",
  437 + "data-points": "Datové body",
  438 + "data-points-storage-days": "Dny uložení datových bodů",
  439 + "email": "Email",
  440 + "email-messages": "Emailové zprávy",
  441 + "email-messages-daily-activity": "Denní aktivita emailových zpráv",
  442 + "email-messages-hourly-activity": "Hodinová aktivita emailových zpráv",
  443 + "email-messages-monthly-activity": "Měsíční aktivita emailových zpráv",
  444 + "exceptions": "Výjimky",
  445 + "executions": "Zpracování",
  446 + "javascript": "JavaScript",
  447 + "javascript-executions": "JavaScript výjimky",
  448 + "javascript-functions": "JavaScript funkce",
  449 + "javascript-functions-daily-activity": "Denní aktivita JavaScript funkcí",
  450 + "javascript-functions-hourly-activity": "Hodinová aktivita JavaScript funkcí",
  451 + "javascript-functions-monthly-activity": "Měsíční aktivita JavaScript funkcí",
  452 + "latest-error": "Poslední chyba",
  453 + "messages": "Zprávy",
  454 + "permanent-failures": "${entityName} permanentní chyby",
  455 + "permanent-timeouts": "${entityName} permanentní timeouty",
  456 + "processing-failures": "${entityName} chyby zpracování",
  457 + "processing-failures-and-timeouts": "Chyby a timeouty zpracování",
  458 + "processing-timeouts": "${entityName} timeouty zpracování",
  459 + "queue-stats": "Statistiky fronty",
  460 + "rule-chain": "Řetěz pravidel",
  461 + "rule-engine": "Engine pro zpracování pravidel",
  462 + "rule-engine-daily-activity": "Denní aktivita enginu pro zpracování pravidel",
  463 + "rule-engine-executions": "Zpracování Enginu pro zpracování pravidel",
  464 + "rule-engine-hourly-activity": "Hodinová aktivita enginu pro zpracování pravidel",
  465 + "rule-engine-monthly-activity": "Měsíční aktivita enginu pro zpracování pravidel",
  466 + "rule-engine-statistics": "Statistiky enginu pro zpracování pravidel",
  467 + "rule-node": "Uzel pravidla",
  468 + "sms": "SMS",
  469 + "sms-messages": "SMS zprávy",
  470 + "sms-messages-daily-activity": "Denní aktivita SMS zpráv",
  471 + "sms-messages-hourly-activity": "Hodinová aktivita SMS zpráv",
  472 + "sms-messages-monthly-activity": "Měsíční aktivita SMS zpráv",
  473 + "successful": "${entityName} úspěšnost",
  474 + "telemetry": "Telemetrie",
  475 + "telemetry-persistence": "Uložení telemetrie",
  476 + "telemetry-persistence-daily-activity": "Denní aktivita uložení telemetrie",
  477 + "telemetry-persistence-hourly-activity": "Hodinová aktivita uložení telemetrie",
  478 + "telemetry-persistence-monthly-activity": "Měsíční aktivita uložení telemetrie",
  479 + "transport": "Přenos",
  480 + "transport-daily-activity": "Denní aktivita přenosu",
  481 + "transport-data-points": "Datové body přenosu",
  482 + "transport-hourly-activity": "Hodinová aktivita přenosu",
  483 + "transport-messages": "Zprávy přenosu",
  484 + "transport-monthly-activity": "Měsíční aktivita přenosu",
  485 + "view-details": "Zobrazit detail",
  486 + "view-statistics": "Zobrazit statistiky"
  487 + },
328 "audit-log": { 488 "audit-log": {
329 "audit": "Audit", 489 "audit": "Audit",
330 "audit-logs": "Záznamy auditu", 490 "audit-logs": "Záznamy auditu",
@@ -363,7 +523,13 @@ @@ -363,7 +523,13 @@
363 "action-data": "Data akce", 523 "action-data": "Data akce",
364 "failure-details": "Detail chyby", 524 "failure-details": "Detail chyby",
365 "search": "Prohledat záznamy auditu", 525 "search": "Prohledat záznamy auditu",
366 - "clear-search": "Vymazat vyhledávání" 526 + "clear-search": "Vymazat vyhledávání",
  527 + "type-assigned-from-tenant": "Odebráno tenantovi",
  528 + "type-assigned-to-tenant": "Přiřazeno tenantovi",
  529 + "type-provision-success": "Zřízení zařízení",
  530 + "type-provision-failure": "Selhání zřízení zařízení",
  531 + "type-timeseries-updated": "Aktualizace telemetrie",
  532 + "type-timeseries-deleted": "Smazání telemetrie"
367 }, 533 },
368 "confirm-on-exit": { 534 "confirm-on-exit": {
369 "message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?", 535 "message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?",
@@ -549,6 +715,7 @@ @@ -549,6 +715,7 @@
549 "title-color": "Barva názvu", 715 "title-color": "Barva názvu",
550 "display-dashboards-selection": "Zobrazit výběr dashboardů", 716 "display-dashboards-selection": "Zobrazit výběr dashboardů",
551 "display-entities-selection": "Zobrazit výběr entit", 717 "display-entities-selection": "Zobrazit výběr entit",
  718 + "display-filters": "Zobrazit filtry",
552 "display-dashboard-timewindow": "Zobrazit časové okno", 719 "display-dashboard-timewindow": "Zobrazit časové okno",
553 "display-dashboard-export": "Zobrazit export", 720 "display-dashboard-export": "Zobrazit export",
554 "import": "Importovat dashboard", 721 "import": "Importovat dashboard",
@@ -615,6 +782,7 @@ @@ -615,6 +782,7 @@
615 "alarm": "Pole alarmu", 782 "alarm": "Pole alarmu",
616 "timeseries-required": "Časové řady entity jsou povinné.", 783 "timeseries-required": "Časové řady entity jsou povinné.",
617 "timeseries-or-attributes-required": "Časové řady / atributy entity jsou povinné.", 784 "timeseries-or-attributes-required": "Časové řady / atributy entity jsou povinné.",
  785 + "alarm-fields-timeseries-or-attributes-required": "Pole alarmu nebo časové řady / atributy jsou povinné.",
618 "maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }", 786 "maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }",
619 "alarm-fields-required": "Pole alarmu jsou povinná.", 787 "alarm-fields-required": "Pole alarmu jsou povinná.",
620 "function-types": "Typy funkcí", 788 "function-types": "Typy funkcí",
@@ -706,6 +874,12 @@ @@ -706,6 +874,12 @@
706 "access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.", 874 "access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.",
707 "rsa-key": "RSA veřejný klíč", 875 "rsa-key": "RSA veřejný klíč",
708 "rsa-key-required": "RSA veřejný klíč je povinný.", 876 "rsa-key-required": "RSA veřejný klíč je povinný.",
  877 + "client-id": "ID klienta",
  878 + "client-id-pattern": "Obsahuje neplatné znaky.",
  879 + "user-name": "Název uživatele",
  880 + "user-name-required": "Název uživatele je povinný.",
  881 + "client-id-or-user-name-necessary": "ID klienta a/nebo název uživatele jsou povinné",
  882 + "password": "Heslo",
709 "secret": "Heslo", 883 "secret": "Heslo",
710 "secret-required": "Heslo je povinné.", 884 "secret-required": "Heslo je povinné.",
711 "device-type": "Typ zařízení", 885 "device-type": "Typ zařízení",
@@ -724,19 +898,183 @@ @@ -724,19 +898,183 @@
724 "details": "Detail", 898 "details": "Detail",
725 "copyId": "Kopírovat Id zařízení", 899 "copyId": "Kopírovat Id zařízení",
726 "copyAccessToken": "Kopírovat přístupový token", 900 "copyAccessToken": "Kopírovat přístupový token",
  901 + "copy-mqtt-authentication": "Kopírovat přístupové údaje MQTT",
727 "idCopiedMessage": "Id zařízení bylo zkopírováno do schránky", 902 "idCopiedMessage": "Id zařízení bylo zkopírováno do schránky",
728 "accessTokenCopiedMessage": "Přístupový token zařízení byl zkopírován do schránky", 903 "accessTokenCopiedMessage": "Přístupový token zařízení byl zkopírován do schránky",
  904 + "mqtt-authentication-copied-message": "MQTT autentizace zařízení byla zkopírována do schránky",
729 "assignedToCustomer": "Přiřazeno zákazníkovi", 905 "assignedToCustomer": "Přiřazeno zákazníkovi",
730 "unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení", 906 "unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení",
731 "unable-delete-device-alias-text": "Alias zařízení '{{deviceAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}", 907 "unable-delete-device-alias-text": "Alias zařízení '{{deviceAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}",
732 "is-gateway": "Je bránou", 908 "is-gateway": "Je bránou",
  909 + "overwrite-activity-time": "Přepsat čas aktivity připojeného zařízení",
733 "public": "Veřejné", 910 "public": "Veřejné",
734 "device-public": "Zařízení je veřejné", 911 "device-public": "Zařízení je veřejné",
735 "select-device": "Vybrat zařízení", 912 "select-device": "Vybrat zařízení",
736 "import": "Importovat zařízení", 913 "import": "Importovat zařízení",
737 "device-file": "Soubor zařízení", 914 "device-file": "Soubor zařízení",
738 "search": "Vyhledat zařízení", 915 "search": "Vyhledat zařízení",
739 - "selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }" 916 + "selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }",
  917 + "device-configuration": "Konfigurace zařízení",
  918 + "transport-configuration": "Konfigurace přenosu",
  919 + "wizard": {
  920 + "device-wizard": "Průvodce zařízením",
  921 + "device-details": "Detail zařízení",
  922 + "new-device-profile": "Vytvořit nový profil zařízení",
  923 + "existing-device-profile": "Vybrat existující profil zařízení",
  924 + "specific-configuration": "Specifická konfigurace",
  925 + "customer-to-assign-device": "Přiřadit zařízení zákazníkovi",
  926 + "add-credential": "Přidat přístupový údaj"
  927 + }
  928 + },
  929 + "device-profile": {
  930 + "device-profile": "Profil zařízení",
  931 + "device-profiles": "Profily zařízení",
  932 + "all-device-profiles": "Všechny",
  933 + "add": "Přidat profil zařízení",
  934 + "edit": "Editovat profil zařízení",
  935 + "device-profile-details": "Detail profilu zařízení",
  936 + "no-device-profiles-text": "Žádné profily zařízení nebyly nalezeny",
  937 + "search": "Vyhledat profily zařízení",
  938 + "selected-device-profiles": "Vybráno { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }",
  939 + "no-device-profiles-matching": "Žádný profil zařízení odpovídající '{{entity}}' nebyl nalezen.",
  940 + "device-profile-required": "Profil zařízení je povinný",
  941 + "idCopiedMessage": "Id profilu zařízení bylo zkopírováno do schránky",
  942 + "set-default": "Učinit profil zařízení defaultním",
  943 + "delete": "Smazat profil zařízení",
  944 + "copyId": "Kopírovat Id profilu zařízení",
  945 + "new-device-profile-name": "Název profilu zařízení",
  946 + "new-device-profile-name-required": "Název profilu zařízení je povinný.",
  947 + "name": "Název",
  948 + "name-required": "Název je povinný.",
  949 + "type": "Typ profilu",
  950 + "type-required": "Typ profilu je povinný.",
  951 + "type-default": "Defaultní",
  952 + "transport-type": "Typ přenosu",
  953 + "transport-type-required": "Typ přenosu je povinný.",
  954 + "transport-type-default": "Defaultní",
  955 + "transport-type-default-hint": "Podporuje základní MQTT, HTTP and CoAP přenos",
  956 + "transport-type-mqtt": "MQTT",
  957 + "transport-type-mqtt-hint": "Umožňuje pokročilé nastavení MQTT přenosu",
  958 + "transport-type-lwm2m": "LWM2M",
  959 + "transport-type-lwm2m-hint": "Typ transportu LWM2M",
  960 + "description": "Popis",
  961 + "default": "Defaultní",
  962 + "profile-configuration": "Konfigurace profilu",
  963 + "transport-configuration": "Konfigurace přenosu",
  964 + "default-rule-chain": "Defaultní řetěz pravidel",
  965 + "select-queue-hint": "Vyberte z rozbalovacího seznamu nebo přidejte vlastní název.",
  966 + "delete-device-profile-title": "Jste si jisti, že chcete smazat profil zařízení '{{deviceProfileName}}'?",
  967 + "delete-device-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil zařízení ani žádná související data obnovit.",
  968 + "delete-device-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }?",
  969 + "delete-device-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily zařízení odstraněny a žádná související data nebude možné obnovit.",
  970 + "set-default-device-profile-title": "Jste si jisti, že chcete profil zařízení '{{deviceProfileName}}' učinit defaultním?",
  971 + "set-default-device-profile-text": "Po potvrzení bude profil zařízení označen jako defaultní a bude použit pro nová zařízení bez specifikovaného profilu.",
  972 + "no-device-profiles-found": "Žádné profily zařízení nebyly nalezeny.",
  973 + "create-new-device-profile": "Vytvořit nový!",
  974 + "mqtt-device-topic-filters": "Filtry MQTT fronty zařízení",
  975 + "mqtt-device-topic-filters-unique": "Filtry MQTT fronty zařízení musí být unikátní.",
  976 + "mqtt-device-payload-type": "MQTT zpráva zařízení",
  977 + "mqtt-device-payload-type-json": "JSON",
  978 + "mqtt-device-payload-type-proto": "Protobuf",
  979 + "mqtt-payload-type-required": "Typ zprávy je povinný.",
  980 + "support-level-wildcards": "Jsou podporovány jednoúrovňové <code>[+]</code> a víceúrovňové <code>[#]</code> zástupné znaky.",
  981 + "telemetry-topic-filter": "Filtr fronty telemetrie",
  982 + "telemetry-topic-filter-required": "Filtr fronty telemetrie je povinný.",
  983 + "attributes-topic-filter": "Filtr atributů fronty",
  984 + "attributes-topic-filter-required": "Filtr atributů fronty je povinný.",
  985 + "telemetry-proto-schema": "Proto schéma telemetrie",
  986 + "telemetry-proto-schema-required": "Proto schéma telemetrie je povinné.",
  987 + "attributes-proto-schema": "Atributy proto schémata",
  988 + "attributes-proto-schema-required": "Atributy proto schémata jsou povinné.",
  989 + "rpc-response-topic-filter": "Filtr fronty RPC odpovědi",
  990 + "rpc-response-topic-filter-required": "Filtr fronty RPC odpovědi je povinný.",
  991 + "not-valid-pattern-topic-filter": "Neplatný vzor filtru fronty",
  992 + "not-valid-single-character": "Neplatné použití jednoúrovňového zástupného znaku",
  993 + "not-valid-multi-character": "Neplatné použití víceúrovňového zástupného znaku",
  994 + "single-level-wildcards-hint": "<code>[+]</code> je vhodný pro jakoukoli úroveň filtru fronty. Př.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
  995 + "multi-level-wildcards-hint": "<code>[#]</code> může nahradit filtr fronty a může se jednat o poslední symbol fronty. Př.: <b>#</b> or <b>v1/devices/me/#</b>.",
  996 + "alarm-rules": "Pravidla alarmu",
  997 + "alarm-rules-with-count": "Pravidla alarmu ({{count}})",
  998 + "no-alarm-rules": "Žádná pravidla alarmu nejsou konfigurována",
  999 + "add-alarm-rule": "Přidat pravidlo alarmu",
  1000 + "edit-alarm-rule": "Editovat pravidlo alarmu",
  1001 + "alarm-type": "Typ alarmu",
  1002 + "alarm-type-required": "Typ alarmu je povinný.",
  1003 + "alarm-type-unique": "Typ alarmu musí být v rámci pravidel alarmu profilu zařízení unikátní.",
  1004 + "create-alarm-pattern": "Vytvořit <b>{{alarmType}}</b> alarm",
  1005 + "create-alarm-rules": "Vytvořit pravidla alarmu",
  1006 + "no-create-alarm-rules": "Nejsou konfigurovány žádné podmínky vytvoření",
  1007 + "add-create-alarm-rule-prompt": "Přidejte prosím pravidlo vytvoření alarmu",
  1008 + "clear-alarm-rule": "Pravidlo zrušení alarmu",
  1009 + "no-clear-alarm-rule": "Není konfigurována žádná podmínka zrušení",
  1010 + "add-create-alarm-rule": "Přidat podmínku vytvoření",
  1011 + "add-clear-alarm-rule": "Přidat podmínku zrušení",
  1012 + "select-alarm-severity": "Vybrat závažnost alarmu",
  1013 + "alarm-severity-required": "Závažnost alarmu je povinná.",
  1014 + "condition-duration": "Doba trvání podmínky",
  1015 + "condition-duration-value": "Hodnota doby trvání",
  1016 + "condition-duration-time-unit": "Jednotka času",
  1017 + "condition-duration-value-range": "Hodnota doby trvání musí být v rozsahu od 1 do 2147483647.",
  1018 + "condition-duration-value-pattern": "Doba trvání musí být celé číslo.",
  1019 + "condition-duration-value-required": "Doba trvání je povinná.",
  1020 + "condition-duration-time-unit-required": "Jednotka času je povinná.",
  1021 + "advanced-settings": "Pokročilá nastavení",
  1022 + "alarm-rule-details": "Detail",
  1023 + "add-alarm-rule-details": "Přidat detail",
  1024 + "propagate-alarm": "Propagovat alarm",
  1025 + "alarm-rule-relation-types-list": "Typy vztahů ke zpropagování",
  1026 + "alarm-rule-relation-types-list-hint": "Pokud nejsou vybrány žádné typy vztahů, alarmy budou propagovány bez filtru typu vztahu.",
  1027 + "alarm-details": "Detail alarmu",
  1028 + "alarm-rule-condition": "Podmínka pravidla alarmu",
  1029 + "enter-alarm-rule-condition-prompt": "Přidejte prosím podmínku pravidla alarmu",
  1030 + "edit-alarm-rule-condition": "Editovat podmínku pravidla alarmu",
  1031 + "device-provisioning": "Zřízení zařízení",
  1032 + "provision-strategy": "Strategie zřízení",
  1033 + "provision-strategy-required": "Strategie zřízení je povinná.",
  1034 + "provision-strategy-disabled": "Zakázáno",
  1035 + "provision-strategy-created-new": "Povolit vytváření nových zařízení",
  1036 + "provision-strategy-check-pre-provisioned": "Zkontrolovat předvytvořená zařízení",
  1037 + "provision-device-key": "Klíč pro zřízení zařízení",
  1038 + "provision-device-key-required": "Klíč pro zřízení zařízení je povinný.",
  1039 + "copy-provision-key": "Kopírovat klíč pro zřízení",
  1040 + "provision-key-copied-message": "Klíč pro zřízení byl zkopírován do schránky",
  1041 + "provision-device-secret": "Heslo pro zřízení zařízení",
  1042 + "provision-device-secret-required": "Heslo pro zřízení zařízení je povinné.",
  1043 + "copy-provision-secret": "Kopírovat heslo pro zřízení",
  1044 + "provision-secret-copied-message": "Heslo pro zřízení zařízení bylo zkopírováno do schránky",
  1045 + "condition": "Podmínka",
  1046 + "condition-type": "Typ podmínky",
  1047 + "condition-type-simple": "Jednoduchá",
  1048 + "condition-type-duration": "Doba trvání",
  1049 + "condition-during": "V průběhu {{during}}",
  1050 + "condition-type-repeating": "Opakování",
  1051 + "condition-type-required": "Typ podmínky je povinný.",
  1052 + "condition-repeating-value": "Počet událostí",
  1053 + "condition-repeating-value-range": "Počet událostí musí být v rozsahu od 1 do 2147483647.",
  1054 + "condition-repeating-value-pattern": "Počet událostí musí být celé číslo.",
  1055 + "condition-repeating-value-required": "Počet událostí je povinný.",
  1056 + "condition-repeat-times": "Opakování { count, plural, 1 {1 krát} other {# krát} }",
  1057 + "schedule-type": "Typ plánovače",
  1058 + "schedule-type-required": "Typ plánovače je povinný.",
  1059 + "schedule": "Časový plán",
  1060 + "edit-schedule": "Editovat časový plán alarmu",
  1061 + "schedule-any-time": "Aktivní neustále",
  1062 + "schedule-specific-time": "Aktivní v konkrétním čase",
  1063 + "schedule-custom": "Vlastní",
  1064 + "schedule-day": {
  1065 + "monday": "Pondělí",
  1066 + "tuesday": "Úterý",
  1067 + "wednesday": "Středa",
  1068 + "thursday": "Čtvrtek",
  1069 + "friday": "Pátek",
  1070 + "saturday": "Sobota",
  1071 + "sunday": "Neděle"
  1072 + },
  1073 + "schedule-days": "Dny",
  1074 + "schedule-time": "Čas",
  1075 + "schedule-time-from": "Od",
  1076 + "schedule-time-to": "Do",
  1077 + "schedule-days-of-week-required": "Musí být vybrán minimálně jeden den v týdnu."
740 }, 1078 },
741 "dialog": { 1079 "dialog": {
742 "close": "Zavřít dialog" 1080 "close": "Zavřít dialog"
@@ -757,7 +1095,7 @@ @@ -757,7 +1095,7 @@
757 "entity-alias": "Alias entity", 1095 "entity-alias": "Alias entity",
758 "unable-delete-entity-alias-title": "Alias entity nebylo možné smazat", 1096 "unable-delete-entity-alias-title": "Alias entity nebylo možné smazat",
759 "unable-delete-entity-alias-text": "Alias entity '{{entityAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}", 1097 "unable-delete-entity-alias-text": "Alias entity '{{entityAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}",
760 - "duplicate-alias-error": "Nalezen dupliticní alias '{{alias}}'.<br>Aliasy entit musí být v rámci dashboardu unikátní.", 1098 + "duplicate-alias-error": "Nalezen duplicitní alias '{{alias}}'.<br>Aliasy entit musí být v rámci dashboardu unikátní.",
761 "missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.", 1099 "missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.",
762 "configure-alias": "Konfigurovat '{{alias}}' alias", 1100 "configure-alias": "Konfigurovat '{{alias}}' alias",
763 "alias": "Alias", 1101 "alias": "Alias",
@@ -794,6 +1132,10 @@ @@ -794,6 +1132,10 @@
794 "type-devices": "Zařízení", 1132 "type-devices": "Zařízení",
795 "list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }", 1133 "list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }",
796 "device-name-starts-with": "Zařízení, jejichž název začíná '{{prefix}}'", 1134 "device-name-starts-with": "Zařízení, jejichž název začíná '{{prefix}}'",
  1135 + "type-device-profile": "Profil zařízení",
  1136 + "type-device-profiles": "Profily zařízení",
  1137 + "list-of-device-profiles": "{ count, plural, 1 {Jeden profil zařízení} other {Seznam # profilů zařízení} }",
  1138 + "device-profile-name-starts-with": "Profily zařízení, jejichž název začíná '{{prefix}}'",
797 "type-asset": "Aktivum", 1139 "type-asset": "Aktivum",
798 "type-assets": "Aktiva", 1140 "type-assets": "Aktiva",
799 "list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }", 1141 "list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }",
@@ -814,6 +1156,10 @@ @@ -814,6 +1156,10 @@
814 "type-tenants": "Tenanti", 1156 "type-tenants": "Tenanti",
815 "list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }", 1157 "list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }",
816 "tenant-name-starts-with": "Tenanti, jejichž název začíná '{{prefix}}'", 1158 "tenant-name-starts-with": "Tenanti, jejichž název začíná '{{prefix}}'",
  1159 + "type-tenant-profile": "Profil tenanta",
  1160 + "type-tenant-profiles": "Profily tenantů",
  1161 + "list-of-tenant-profiles": "{ count, plural, 1 {Jeden profil tenanta} other {Seznam # profilů tenantů} }",
  1162 + "tenant-profile-name-starts-with": "Profily tenantů, jejichž název začíná '{{prefix}}'",
817 "type-customer": "Zákazník", 1163 "type-customer": "Zákazník",
818 "type-customers": "Zákazníci", 1164 "type-customers": "Zákazníci",
819 "list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }", 1165 "list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }",
@@ -840,6 +1186,8 @@ @@ -840,6 +1186,8 @@
840 "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'", 1186 "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'",
841 "type-current-customer": "Stávající zákazník", 1187 "type-current-customer": "Stávající zákazník",
842 "type-current-tenant": "Stávající tenant", 1188 "type-current-tenant": "Stávající tenant",
  1189 + "type-current-user": "Stávající uživatel",
  1190 + "type-current-user-owner": "Vlastník stávajícího uživatele",
843 "search": "Vyhledat entity", 1191 "search": "Vyhledat entity",
844 "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno", 1192 "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno",
845 "entity-name": "Název entity", 1193 "entity-name": "Název entity",
@@ -847,7 +1195,8 @@ @@ -847,7 +1195,8 @@
847 "details": "Detail entity", 1195 "details": "Detail entity",
848 "no-entities-prompt": "Žádné entity nebyly nalezeny", 1196 "no-entities-prompt": "Žádné entity nebyly nalezeny",
849 "no-data": "Nelze zobrazit žádná data", 1197 "no-data": "Nelze zobrazit žádná data",
850 - "columns-to-display": "Zobrazit sloupce" 1198 + "columns-to-display": "Zobrazit sloupce",
  1199 + "type-api-usage-state": "Stav využití API"
851 }, 1200 },
852 "entity-field": { 1201 "entity-field": {
853 "created-time": "Datum vytvoření", 1202 "created-time": "Datum vytvoření",
@@ -1048,7 +1397,7 @@ @@ -1048,7 +1397,7 @@
1048 "anonymous": "Anonymní", 1397 "anonymous": "Anonymní",
1049 "basic": "Základní", 1398 "basic": "Základní",
1050 "pem": "PEM", 1399 "pem": "PEM",
1051 - "ca-cert": "soubor CA certifikátu *", 1400 + "ca-cert": "Soubor CA certifikátu *",
1052 "private-key": "Soubor privátního klíče *", 1401 "private-key": "Soubor privátního klíče *",
1053 "cert": "Soubor certifikátu *", 1402 "cert": "Soubor certifikátu *",
1054 "no-file": "Žádný soubor nebyl vybrán.", 1403 "no-file": "Žádný soubor nebyl vybrán.",
@@ -1154,6 +1503,93 @@ @@ -1154,6 +1503,93 @@
1154 "file": "Soubor rozšíření", 1503 "file": "Soubor rozšíření",
1155 "invalid-file-error": "Neplatný soubor rozšíření" 1504 "invalid-file-error": "Neplatný soubor rozšíření"
1156 }, 1505 },
  1506 + "filter": {
  1507 + "add": "Přidat filtr",
  1508 + "edit": "Editovat filtr",
  1509 + "name": "Název filtru",
  1510 + "name-required": "Název filtru je povinný.",
  1511 + "duplicate-filter": "Filtr s identickým názvem již existuje.",
  1512 + "filters": "Filtry",
  1513 + "unable-delete-filter-title": "Smazat filtr není možné",
  1514 + "unable-delete-filter-text": "Filtr '{{filter}}' není možné smazat, protože je používán následujícím widgetem(y):<br/>{{widgetsList}}",
  1515 + "duplicate-filter-error": "Nalezen duplicitní filtr '{{filter}}'.<br>Filtry musí být v rámci dashboardu unikátní.",
  1516 + "missing-key-filters-error": "U filtru '{{filter}}' chybí klíčové filtry.",
  1517 + "filter": "Filtr",
  1518 + "editable": "Editovatelné",
  1519 + "no-filters-found": "Žádné filtry nebyly nalezeny.",
  1520 + "no-filter-text": "Není specifikován žádný filtr",
  1521 + "add-filter-prompt": "Přidejte prosím filtr",
  1522 + "no-filter-matching": "'{{filter}}' nebyl nalezen.",
  1523 + "create-new-filter": "Vytvořit nový!",
  1524 + "filter-required": "Filtr je povinný.",
  1525 + "operation": {
  1526 + "operation": "Operace",
  1527 + "equal": "je rovno",
  1528 + "not-equal": "není rovno",
  1529 + "starts-with": "začíná na",
  1530 + "ends-with": "končí na",
  1531 + "contains": "obsahuje",
  1532 + "not-contains": "neobsahuje",
  1533 + "greater": "větší než",
  1534 + "less": "menší než",
  1535 + "greater-or-equal": "větší nebo rovno",
  1536 + "less-or-equal": "menší nebo rovno",
  1537 + "and": "a",
  1538 + "or": "nebo"
  1539 + },
  1540 + "ignore-case": "ignorovat velikost písmen",
  1541 + "value": "Hodnota",
  1542 + "remove-filter": "Odebrat filtr",
  1543 + "preview": "Náhled filtru",
  1544 + "no-filters": "Nejsou konfigurovány žádné filtry",
  1545 + "add-filter": "Přidat filtr",
  1546 + "add-complex-filter": "Přidat komplexní filtr",
  1547 + "add-complex": "Přidat komplex",
  1548 + "complex-filter": "Komplexní filtr",
  1549 + "edit-complex-filter": "Editovat komplexní filtr",
  1550 + "edit-filter-user-params": "Editovat filtr predikátu parametrů uživatele",
  1551 + "filter-user-params": "Filtr predikátu parametrů uživatele",
  1552 + "user-parameters": "Parametry uživatele",
  1553 + "display-label": "Zobrazované označení",
  1554 + "autogenerated-label": "Automaticky vygenerovat označení",
  1555 + "order-priority": "Priority pořadí polí",
  1556 + "key-filter": "Klíčový filtr",
  1557 + "key-filters": "Klíčové filtry",
  1558 + "key-name": "Název klíče",
  1559 + "key-name-required": "Název klíče je povinný.",
  1560 + "key-type": {
  1561 + "key-type": "Typ klíče",
  1562 + "attribute": "Atribut",
  1563 + "timeseries": "Časové řady",
  1564 + "entity-field": "Pole entity"
  1565 + },
  1566 + "value-type": {
  1567 + "value-type": "Typ hodnoty",
  1568 + "string": "Řetězec",
  1569 + "numeric": "Číslo",
  1570 + "boolean": "Pravdivostní hodnota",
  1571 + "date-time": "Datum a čas"
  1572 + },
  1573 + "value-type-required": "Typ hodnoty klíče je povinný.",
  1574 + "key-value-type-change-title": "Jste si jisti, že chcete změnit typ klíče hodnoty?",
  1575 + "key-value-type-change-message": "Pokud potvrdíte nový typ hodnoty, všechny zadané klíčové filtry budou odstraněny.",
  1576 + "no-key-filters": "Nejsou konfigurovány žádné klíčové filtry",
  1577 + "add-key-filter": "Přidat klíčový filtr",
  1578 + "remove-key-filter": "Odebrat klíčový filtr",
  1579 + "edit-key-filter": "Editovat klíčový filtr",
  1580 + "date": "Datum",
  1581 + "time": "Čas",
  1582 + "current-tenant": "Stávající tenant",
  1583 + "current-customer": "Stávající zákazník",
  1584 + "current-user": "Stávající uživatel",
  1585 + "current-device": "Stávající zařízení",
  1586 + "default-value": "Defaultní hodnota",
  1587 + "dynamic-source-type": "Dynamický typ zdroje",
  1588 + "no-dynamic-value": "Žádná dynamická hodnota",
  1589 + "source-attribute": "Atribut zdroje",
  1590 + "switch-to-dynamic-value": "Přepnout na dynamickou hodnotu",
  1591 + "switch-to-default-value": "Přepnout na defaultní hodnotu"
  1592 + },
1157 "fullscreen": { 1593 "fullscreen": {
1158 "expand": "Rozšířit do režimu celé obrazovky", 1594 "expand": "Rozšířit do režimu celé obrazovky",
1159 "exit": "Ukončit režim celé obrazovky", 1595 "exit": "Ukončit režim celé obrazovky",
@@ -1286,6 +1722,7 @@ @@ -1286,6 +1722,7 @@
1286 "entity-field": "Pole entity", 1722 "entity-field": "Pole entity",
1287 "access-token": "Přístupový token", 1723 "access-token": "Přístupový token",
1288 "isgateway": "Je bránou", 1724 "isgateway": "Je bránou",
  1725 + "activity-time-from-gateway-device": "Čas aktivity ze zařízení brány",
1289 "description": "Popis" 1726 "description": "Popis"
1290 }, 1727 },
1291 "stepper-text":{ 1728 "stepper-text":{
@@ -1329,6 +1766,7 @@ @@ -1329,6 +1766,7 @@
1329 "legend": { 1766 "legend": {
1330 "direction": "Směr legendy", 1767 "direction": "Směr legendy",
1331 "position": "Pozice legendy", 1768 "position": "Pozice legendy",
  1769 + "sort-legend": "Setřídit datové klíče v legendě",
1332 "show-max": "Zobrazit max hodnotu", 1770 "show-max": "Zobrazit max hodnotu",
1333 "show-min": "Zobrazit min hodnotu", 1771 "show-min": "Zobrazit min hodnotu",
1334 "show-avg": "Zobrazit průměrnou hodnotu", 1772 "show-avg": "Zobrazit průměrnou hodnotu",
@@ -1525,6 +1963,12 @@ @@ -1525,6 +1963,12 @@
1525 "help": "Nápověda", 1963 "help": "Nápověda",
1526 "reset-debug-mode": "Resetovat režim ladění na všech uzlech" 1964 "reset-debug-mode": "Resetovat režim ladění na všech uzlech"
1527 }, 1965 },
  1966 + "timezone": {
  1967 + "timezone": "Časová zóna",
  1968 + "select-timezone": "Vyberte časovou zónu",
  1969 + "no-timezones-matching": "žádné časové zóny odpovídající '{{timezone}}' nebyly nalezeny.",
  1970 + "timezone-required": "Časová zóna je povinná."
  1971 + },
1528 "queue": { 1972 "queue": {
1529 "select_name": "Vybrat název fronty", 1973 "select_name": "Vybrat název fronty",
1530 "name": "Název fronty", 1974 "name": "Název fronty",
@@ -1563,6 +2007,87 @@ @@ -1563,6 +2007,87 @@
1563 "isolated-tb-core-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta", 2007 "isolated-tb-core-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta",
1564 "isolated-tb-rule-engine-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta" 2008 "isolated-tb-rule-engine-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta"
1565 }, 2009 },
  2010 + "tenant-profile": {
  2011 + "tenant-profile": "Profil tenanta",
  2012 + "tenant-profiles": "Profily tenantů",
  2013 + "add": "Přidat profil tenanta",
  2014 + "edit": "Editovat profil tenanta",
  2015 + "tenant-profile-details": "Detail profilu tenanta",
  2016 + "no-tenant-profiles-text": "Nebyly nalezeny žádné profily tenantů",
  2017 + "search": "Vyhledat profily tenantů",
  2018 + "selected-tenant-profiles": "Vybráno { count, plural, 1 {1 profilů tenantů} other {# profilů tenantů} }",
  2019 + "no-tenant-profiles-matching": "Žádné profily tenantů odpovídající '{{entity}}' nebyly nalezeny.",
  2020 + "tenant-profile-required": "Profil tenanta je povinný",
  2021 + "idCopiedMessage": "Id profilu tenanta bylo zkopírováno do schránky",
  2022 + "set-default": "Učinit profil tenanta defaultním",
  2023 + "delete": "Smazat profil tenanta",
  2024 + "copyId": "Kopírovat Id profilu tenanta",
  2025 + "name": "Název",
  2026 + "name-required": "Název je povinný.",
  2027 + "data": "Data profilu",
  2028 + "profile-configuration": "Konfigurace profilu",
  2029 + "description": "Popis",
  2030 + "default": "Defaultní",
  2031 + "delete-tenant-profile-title": "Jste si jisti, že chcete smazat profil tenanta '{{tenantProfileName}}'?",
  2032 + "delete-tenant-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil tenanta ani žádná související data obnovit.",
  2033 + "delete-tenant-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil tenanta} other {# profilů tenanta} }?",
  2034 + "delete-tenant-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily tenantů odstraněny a žádná související data nebude možné obnovit.",
  2035 + "set-default-tenant-profile-title": "Jste si jisti, že chcete učinit profil tenanta '{{tenantProfileName}}' defaultním?",
  2036 + "set-default-tenant-profile-text": "Po potvrzení bude profil tenanta označen jako defaultní a bude použit pro nové tenanty bez specifikovaného profilu.",
  2037 + "no-tenant-profiles-found": "Nebyly nalezeny žádné profily tenantů.",
  2038 + "create-new-tenant-profile": "Vytvořit nový!",
  2039 + "maximum-devices": "Maximální počet zařízení (0 - neomezeno)",
  2040 + "maximum-devices-required": "Maximální počet zařízení je povinný.",
  2041 + "maximum-devices-range": "Minimální počet zařízení nemůže být záporný",
  2042 + "maximum-assets": "Maximální počet aktiv (0 - neomezeno)",
  2043 + "maximum-assets-required": "Maximální počet aktiv je povinný.",
  2044 + "maximum-assets-range": "Maximální počet aktiv nemůže být záporný",
  2045 + "maximum-customers": "Maximální počet zákazníků (0 - neomezeno)",
  2046 + "maximum-customers-required": "Maximální počet zákazníkůje povinný.",
  2047 + "maximum-customers-range": "Maximální počet zákazníků nemůže být záporný",
  2048 + "maximum-users": "Maximální počet uživatelů (0 - neomezeno)",
  2049 + "maximum-users-required": "Maximální počet uživatelů je povinný.",
  2050 + "maximum-users-range": "Maximální počet uživatelů nemůže být záporný",
  2051 + "maximum-dashboards": "Maximální počet dashboardů (0 - neomezeno)",
  2052 + "maximum-dashboards-required": "Maximální počet dashboardů je povinný.",
  2053 + "maximum-dashboards-range": "Maximální počet dashboardů nemůže být záporný",
  2054 + "maximum-rule-chains": "Maximální počet řetězů pravidel (0 - neomezeno)",
  2055 + "maximum-rule-chains-required": "Maximální počet řetězů pravidel je povinný.",
  2056 + "maximum-rule-chains-range": "Maximální počet řetězů pravidel nemůže být záporný",
  2057 + "transport-tenant-msg-rate-limit": "Limit přenosu zpráv tenanta.",
  2058 + "transport-tenant-telemetry-msg-rate-limit": "Limit přenosu zpráv telemetrie tenanta.",
  2059 + "transport-tenant-telemetry-data-points-rate-limit": "Limit přenosu datových bodů telemetrie tenanta.",
  2060 + "transport-device-msg-rate-limit": "Limit přenosu zpráv zařízení.",
  2061 + "transport-device-telemetry-msg-rate-limit": "Limit přenosu zpráv zařízení telemetrie tenanta.",
  2062 + "transport-device-telemetry-data-points-rate-limit": "Limit přenosu datových bodů zařízení telemetrie tenanta.",
  2063 + "max-transport-messages": "Maximální počet zpráv přenosu (0 - neomezeno)",
  2064 + "max-transport-messages-required": "Maximální počet zpráv přenosu je povinný.",
  2065 + "max-transport-messages-range": "Maximální počet zpráv přenosu nemůže být záporný",
  2066 + "max-transport-data-points": "Maximální počet datových bodů přenosu (0 - neomezeno)",
  2067 + "max-transport-data-points-required": "Maximální počet datových bodů přenosu je povinný.",
  2068 + "max-transport-data-points-range": "Maximální počet datových bodů přenosu nemůže být záporný",
  2069 + "max-r-e-executions": "Maximální počet zpracování enginu pro zpracování pravidel (0 - neomezeno)",
  2070 + "max-r-e-executions-required": "Maximální počet zpracování enginu pro zpracování pravidel je povinný.",
  2071 + "max-r-e-executions-range": "Maximální počet zpracování enginu pro zpracování pravidel nemůže být záporný",
  2072 + "max-j-s-executions": "Maximální počet JavaScript zpracování (0 - neomezeno)",
  2073 + "max-j-s-executions-required": "Maximální počet JavaScript zpracování je povinný.",
  2074 + "max-j-s-executions-range": "Maximální počet JavaScript zpracování nemůže být záporný",
  2075 + "max-d-p-storage-days": "Maximální počet dnů uložení datových bodů (0 - neomezeno)",
  2076 + "max-d-p-storage-days-required": "Maximální počet dnů uložení datových bodů je povinný.",
  2077 + "max-d-p-storage-days-range": "Maximální počet dnů uložení datových bodů nemůže být záporný",
  2078 + "default-storage-ttl-days": "Defaultní počet dnů TTL úložiště (0 - neomezeno)",
  2079 + "default-storage-ttl-days-required": "Defaultní počet dnů TTL úložiště je povinný.",
  2080 + "default-storage-ttl-days-range": "Defaultní počet dnů TTL úložiště nemůže být záporný",
  2081 + "max-rule-node-executions-per-message": "Maximální počet zpracování uzlů pravidel na zprávu (0 - neomezeno)",
  2082 + "max-rule-node-executions-per-message-required": "Maximální počet zpracování uzlů pravidel na zprávu je povinný.",
  2083 + "max-rule-node-executions-per-message-range": "Maximální počet zpracování uzlů pravidel na zprávu nemůže být záporný",
  2084 + "max-emails": "Maximální počet odeslaných emailů (0 - neomezeno)",
  2085 + "max-emails-required": "Maximální počet odeslaných emailů je povinný.",
  2086 + "max-emails-range": "Maximální počet odeslaných emailů nemůže být záporný",
  2087 + "max-sms": "Maximální počet odeslaných SMS (0 - neomezeno)",
  2088 + "max-sms-required": "Maximální počet odeslaných SMS je povinný.",
  2089 + "max-sms-range": "Maximální počet odeslaných SMS nemůže být záporný"
  2090 + },
1566 "timeinterval": { 2091 "timeinterval": {
1567 "seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }", 2092 "seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }",
1568 "minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }", 2093 "minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }",
@@ -1574,8 +2099,14 @@ @@ -1574,8 +2099,14 @@
1574 "seconds": "Vteřiny", 2099 "seconds": "Vteřiny",
1575 "advanced": "Rozšířené" 2100 "advanced": "Rozšířené"
1576 }, 2101 },
  2102 + "timeunit": {
  2103 + "seconds": "Vteřiny",
  2104 + "minutes": "Minuty",
  2105 + "hours": "Hodiny",
  2106 + "days": "Dny"
  2107 + },
1577 "timewindow": { 2108 "timewindow": {
1578 - "days": "{ days, plural, 1 { den } other {# days } }", 2109 + "days": "{ days, plural, 1 { den } other {# d } }",
1579 "hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }", 2110 "hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }",
1580 "minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }", 2111 "minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }",
1581 "seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }", 2112 "seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }",
@@ -1694,6 +2225,7 @@ @@ -1694,6 +2225,7 @@
1694 "type": "Typ widgetu", 2225 "type": "Typ widgetu",
1695 "resources": "Zdroje", 2226 "resources": "Zdroje",
1696 "resource-url": "JavaScript/CSS URL", 2227 "resource-url": "JavaScript/CSS URL",
  2228 + "resource-is-module": "Je modulem",
1697 "remove-resource": "Odebrat zdroj", 2229 "remove-resource": "Odebrat zdroj",
1698 "add-resource": "Přidat zdroj", 2230 "add-resource": "Přidat zdroj",
1699 "html": "HTML", 2231 "html": "HTML",
@@ -1711,7 +2243,10 @@ @@ -1711,7 +2243,10 @@
1711 "widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!", 2243 "widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!",
1712 "add": "Přidat widget", 2244 "add": "Přidat widget",
1713 "undo": "Vrátit změny widgetu", 2245 "undo": "Vrátit změny widgetu",
1714 - "export": "Exportovat widget" 2246 + "export": "Exportovat widget",
  2247 + "no-data": "Nejsou k dispozici žádná data pro zobrazení ve widgetu",
  2248 + "data-overflow": "Widget zobrazuje {{count}} z {{total}} entit",
  2249 + "alarm-data-overflow": "Widget zobrazuje alarmy {{allowedEntities}} (maxima možných) entit z {{totalEntities}} entit"
1715 }, 2250 },
1716 "widget-action": { 2251 "widget-action": {
1717 "header-button": "Tlačítko hlavičky widgetu", 2252 "header-button": "Tlačítko hlavičky widgetu",
@@ -1724,7 +2259,14 @@ @@ -1724,7 +2259,14 @@
1724 "target-dashboard-state-required": "Cílový stav dashboardu je povinný", 2259 "target-dashboard-state-required": "Cílový stav dashboardu je povinný",
1725 "set-entity-from-widget": "Nastavit entitu z widgetu", 2260 "set-entity-from-widget": "Nastavit entitu z widgetu",
1726 "target-dashboard": "Cílový dashboard", 2261 "target-dashboard": "Cílový dashboard",
1727 - "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)" 2262 + "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)",
  2263 + "open-in-separate-dialog": "Otevřít v samostatném okně",
  2264 + "dialog-title": "Název okna",
  2265 + "dialog-hide-dashboard-toolbar": "Skrýt v okně nástrojovou lištu dashboardu",
  2266 + "dialog-width": "Šířka okna v procentech vzhledem k šířce obrazovky",
  2267 + "dialog-height": "Výška okna v procentech vzhledem k výšce obrazovky",
  2268 + "dialog-size-range-error": "Hodnota procentuální velikosti musí být v rozsahu od 1 do 100.",
  2269 + "open-new-browser-tab": "Otevřít na nové záložce prohlížeče"
1728 }, 2270 },
1729 "widgets-bundle": { 2271 "widgets-bundle": {
1730 "current": "Vybraná kategorie", 2272 "current": "Vybraná kategorie",
@@ -1891,8 +2433,11 @@ @@ -1891,8 +2433,11 @@
1891 "entity-coordinate-required": "Obě pole, zeměpisná šířka i zeměpisná délka, jsou povinná", 2433 "entity-coordinate-required": "Obě pole, zeměpisná šířka i zeměpisná délka, jsou povinná",
1892 "entity-timeseries-required": "Časové řady entity jsou povinné", 2434 "entity-timeseries-required": "Časové řady entity jsou povinné",
1893 "get-location": "Získat aktuální polohu", 2435 "get-location": "Získat aktuální polohu",
  2436 + "invalid-date": "Neplatné datum",
1894 "latitude": "Zeměpisná šířka", 2437 "latitude": "Zeměpisná šířka",
1895 "longitude": "Zeměpisná délka", 2438 "longitude": "Zeměpisná délka",
  2439 + "min-value-error": "Minimální hodnota je {{value}}",
  2440 + "max-value-error": "Maximální hodnota je {{value}}",
1896 "not-allowed-entity": "Vybraná entita nemůže mít sdílené atributy", 2441 "not-allowed-entity": "Vybraná entita nemůže mít sdílené atributy",
1897 "no-attribute-selected": "Není vybrán žádný atribut", 2442 "no-attribute-selected": "Není vybrán žádný atribut",
1898 "no-datakey-selected": "Není vybrán žádný datový klíč", 2443 "no-datakey-selected": "Není vybrán žádný datový klíč",
@@ -1900,7 +2445,10 @@ @@ -1900,7 +2445,10 @@
1900 "no-entity-selected": "Není vybrána žádná entita", 2445 "no-entity-selected": "Není vybrána žádná entita",
1901 "no-image": "Žádný obrázek", 2446 "no-image": "Žádný obrázek",
1902 "no-support-geolocation": "Váš prohlížeč nepodporuje geolokaci", 2447 "no-support-geolocation": "Váš prohlížeč nepodporuje geolokaci",
1903 - "no-support-web-camera": "Žádná podporovaná webová kamera", 2448 + "no-support-web-camera": "Váš prohlížeč nepodporuje kamery",
  2449 + "enable-https-use-widget": "Prosím povolte HTTPS abyste mohli používat tento widget",
  2450 + "no-found-your-camera": "Nelze nalézt vyši kameru",
  2451 + "no-permission-camera": "Přístup byl zakázán uživatelem / Tato stránka nemá oprávnění použít kameru",
1904 "no-timeseries-selected": "Nejsou vybrány žádné časové řady", 2452 "no-timeseries-selected": "Nejsou vybrány žádné časové řady",
1905 "secret-key": "Tajný klíč", 2453 "secret-key": "Tajný klíč",
1906 "secret-key-required": "Tajný klíč je povinný", 2454 "secret-key-required": "Tajný klíč je povinný",
@@ -74,6 +74,7 @@ @@ -74,6 +74,7 @@
74 "admin": { 74 "admin": {
75 "general": "General", 75 "general": "General",
76 "general-settings": "General Settings", 76 "general-settings": "General Settings",
  77 + "home-settings": "Home Settings",
77 "outgoing-mail": "Mail Server", 78 "outgoing-mail": "Mail Server",
78 "outgoing-mail-settings": "Outgoing Mail Server Settings", 79 "outgoing-mail-settings": "Outgoing Mail Server Settings",
79 "system-settings": "System Settings", 80 "system-settings": "System Settings",
@@ -764,7 +765,9 @@ @@ -764,7 +765,9 @@
764 "select-state": "Select target state", 765 "select-state": "Select target state",
765 "state-controller": "State controller", 766 "state-controller": "State controller",
766 "search": "Search dashboards", 767 "search": "Search dashboards",
767 - "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected" 768 + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected",
  769 + "home-dashboard": "Home dashboard",
  770 + "home-dashboard-hide-toolbar": "Hide home dashboard toolbar"
768 }, 771 },
769 "datakey": { 772 "datakey": {
770 "settings": "Settings", 773 "settings": "Settings",