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 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 29 "alias": "doughnut_chart_js",
46 30 "name": "Doughnut - Chart.js",
47 31 "descriptor": {
... ... @@ -71,7 +55,7 @@
71 55 "resources": [],
72 56 "templateHtml": "",
73 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 59 "settingsSchema": "{}\n",
76 60 "dataKeySettingsSchema": "{}\n",
77 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 122 }
139 123 },
140 124 {
141   - "alias": "timeseries_bars_flot",
142   - "name": "Timeseries Bars - Flot",
  125 + "alias": "state_chart",
  126 + "name": "State Chart",
143 127 "descriptor": {
144 128 "type": "timeseries",
145 129 "sizeX": 8,
... ... @@ -147,15 +131,15 @@
147 131 "resources": [],
148 132 "templateHtml": "",
149 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 135 "settingsSchema": "{}",
152 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 143 "descriptor": {
160 144 "type": "timeseries",
161 145 "sizeX": 8,
... ... @@ -163,11 +147,27 @@
163 147 "resources": [],
164 148 "templateHtml": "",
165 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 151 "settingsSchema": "{}",
168 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 +}
\ No newline at end of file
... ...
  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 +}
\ No newline at end of file
... ...
... ... @@ -15,6 +15,8 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 20 import org.springframework.beans.factory.annotation.Value;
19 21 import org.springframework.http.HttpStatus;
20 22 import org.springframework.security.access.prepost.PreAuthorize;
... ... @@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer;
30 32 import org.thingsboard.server.common.data.Dashboard;
31 33 import org.thingsboard.server.common.data.DashboardInfo;
32 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 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 40 import org.thingsboard.server.common.data.audit.ActionType;
35 41 import org.thingsboard.server.common.data.exception.ThingsboardException;
36 42 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -39,7 +45,9 @@ import org.thingsboard.server.common.data.id.TenantId;
39 45 import org.thingsboard.server.common.data.page.PageData;
40 46 import org.thingsboard.server.common.data.page.PageLink;
41 47 import org.thingsboard.server.common.data.page.TimePageLink;
  48 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
42 49 import org.thingsboard.server.queue.util.TbCoreComponent;
  50 +import org.thingsboard.server.service.security.model.SecurityUser;
43 51 import org.thingsboard.server.service.security.permission.Operation;
44 52 import org.thingsboard.server.service.security.permission.Resource;
45 53
... ... @@ -53,6 +61,9 @@ public class DashboardController extends BaseController {
53 61
54 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 67 @Value("${dashboard.max_datapoints_limit}")
57 68 private long maxDatapointsLimit;
58 69
... ... @@ -472,4 +483,100 @@ public class DashboardController extends BaseController {
472 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 185 case "3.2.0":
186 186 log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
187 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 190 log.info("Updating system data...");
189 191 systemDataLoaderService.updateSystemWidgets();
190 192 break;
... ...
... ... @@ -197,7 +197,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
197 197 generalSettings.setKey("general");
198 198 ObjectNode node = objectMapper.createObjectNode();
199 199 node.put("baseUrl", "http://localhost:8080");
200   - node.put("prohibitDifferentUrl", true);
  200 + node.put("prohibitDifferentUrl", false);
201 201 generalSettings.setJsonValue(node);
202 202 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
203 203
... ... @@ -438,6 +438,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
438 438 this.deleteSystemWidgetBundle("input_widgets");
439 439 this.deleteSystemWidgetBundle("date");
440 440 this.deleteSystemWidgetBundle("entity_admin_widgets");
  441 + this.deleteSystemWidgetBundle("navigation_widgets");
441 442 installScripts.loadSystemWidgets();
442 443 }
443 444
... ...
... ... @@ -15,11 +15,20 @@
15 15 */
16 16 package org.thingsboard.server.service.install;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 19 import org.springframework.context.annotation.Profile;
19 20 import org.springframework.stereotype.Service;
20 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 30 @Service
  31 +@Slf4j
23 32 @HsqlDao
24 33 @Profile("install")
25 34 public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
... ... @@ -27,5 +36,21 @@ public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSe
27 36 protected HsqlEntityDatabaseSchemaService() {
28 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 30 @Slf4j
31 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 35 @Value("${spring.datasource.url}")
36 36 protected String dbUrl;
... ... @@ -42,7 +42,7 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
42 42 protected String dbPassword;
43 43
44 44 @Autowired
45   - private InstallScripts installScripts;
  45 + protected InstallScripts installScripts;
46 46
47 47 private final String schemaSql;
48 48 private final String schemaIdxSql;
... ...
... ... @@ -59,6 +59,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
59 59 import org.thingsboard.server.queue.util.TbCoreComponent;
60 60 import org.thingsboard.server.service.queue.TbClusterService;
61 61 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
  62 +import org.thingsboard.server.utils.EventDeduplicationExecutor;
62 63
63 64 import javax.annotation.Nullable;
64 65 import javax.annotation.PostConstruct;
... ... @@ -126,13 +127,13 @@ public class DefaultDeviceStateService implements DeviceStateService {
126 127 @Getter
127 128 private int initFetchPackSize;
128 129
129   - private volatile boolean clusterUpdatePending = false;
130   -
131 130 private ListeningScheduledExecutorService queueExecutor;
132 131 private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>();
133 132 private final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
134 133 private final ConcurrentMap<DeviceId, Long> deviceLastReportedActivity = new ConcurrentHashMap<>();
135 134 private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>();
  135 + private volatile EventDeduplicationExecutor<Set<TopicPartitionInfo>> deduplicationExecutor;
  136 +
136 137
137 138 public DefaultDeviceStateService(TenantService tenantService, DeviceService deviceService,
138 139 AttributesService attributesService, TimeseriesService tsService,
... ... @@ -155,6 +156,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
155 156 // Should be always single threaded due to absence of locks.
156 157 queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-state")));
157 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 162 @PreDestroy
... ... @@ -292,25 +294,14 @@ public class DefaultDeviceStateService implements DeviceStateService {
292 294 }
293 295 }
294 296
295   - volatile Set<TopicPartitionInfo> pendingPartitions;
296   -
297 297 @Override
298 298 public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
299 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 305 try {
315 306 log.info("CURRENT PARTITIONS: {}", partitionedDevices.keySet());
316 307 log.info("NEW PARTITIONS: {}", pendingPartitions);
... ...
... ... @@ -302,7 +302,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
302 302 Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
303 303 TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
304 304 attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
305   - ctx.setAndResolveQuery(cmd.getQuery());
  305 + if (cmd.getQuery() != null) {
  306 + ctx.setAndResolveQuery(cmd.getQuery());
  307 + }
306 308 sessionSubs.put(cmd.getCmdId(), ctx);
307 309 return ctx;
308 310 }
... ...
... ... @@ -107,7 +107,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
107 107 public void setAndResolveQuery(T query) {
108 108 dynamicValues.clear();
109 109 this.query = query;
110   - if (query.getKeyFilters() != null) {
  110 + if (query != null && query.getKeyFilters() != null) {
111 111 for (KeyFilter filter : query.getKeyFilters()) {
112 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 19 import org.thingsboard.server.common.data.alarm.Alarm;
20 20 import org.thingsboard.server.common.data.alarm.AlarmInfo;
21 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 24 import org.thingsboard.server.common.data.id.CustomerId;
23 25 import org.thingsboard.server.common.data.id.EntityId;
24 26 import org.thingsboard.server.common.data.id.TenantId;
25 27 import org.thingsboard.server.common.data.page.PageData;
26 28 import org.thingsboard.server.common.data.query.AlarmData;
27   -import org.thingsboard.server.common.data.query.AlarmDataPageLink;
28 29 import org.thingsboard.server.common.data.query.AlarmDataQuery;
29 30 import org.thingsboard.server.dao.Dao;
30 31
31 32 import java.util.Collection;
  33 +import java.util.Set;
32 34 import java.util.UUID;
33 35
34 36 /**
... ... @@ -48,4 +50,6 @@ public interface AlarmDao extends Dao<Alarm> {
48 50
49 51 PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId,
50 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 39 import org.thingsboard.server.common.data.id.EntityId;
40 40 import org.thingsboard.server.common.data.id.TenantId;
41 41 import org.thingsboard.server.common.data.page.PageData;
42   -import org.thingsboard.server.common.data.page.TimePageLink;
43 42 import org.thingsboard.server.common.data.query.AlarmData;
44 43 import org.thingsboard.server.common.data.query.AlarmDataPageLink;
45 44 import org.thingsboard.server.common.data.query.AlarmDataQuery;
  45 +import org.thingsboard.server.common.data.query.DeviceTypeFilter;
46 46 import org.thingsboard.server.common.data.relation.EntityRelation;
47 47 import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
48 48 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
... ... @@ -60,7 +60,6 @@ import javax.annotation.PreDestroy;
60 60 import java.util.ArrayList;
61 61 import java.util.Collection;
62 62 import java.util.Collections;
63   -import java.util.Comparator;
64 63 import java.util.LinkedHashSet;
65 64 import java.util.List;
66 65 import java.util.Set;
... ... @@ -316,37 +315,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
316 315 @Override
317 316 public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
318 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 330 private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) {
... ...
... ... @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable;
20 20 import org.springframework.data.jpa.repository.Query;
21 21 import org.springframework.data.repository.CrudRepository;
22 22 import org.springframework.data.repository.query.Param;
  23 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
23 24 import org.thingsboard.server.common.data.alarm.AlarmStatus;
24 25 import org.thingsboard.server.dao.model.sql.AlarmEntity;
25 26 import org.thingsboard.server.dao.model.sql.AlarmInfoEntity;
... ... @@ -75,4 +76,12 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
75 76 @Param("searchText") String searchText,
76 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 24 import org.thingsboard.server.common.data.alarm.Alarm;
25 25 import org.thingsboard.server.common.data.alarm.AlarmInfo;
26 26 import org.thingsboard.server.common.data.alarm.AlarmQuery;
  27 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
27 28 import org.thingsboard.server.common.data.alarm.AlarmStatus;
28 29 import org.thingsboard.server.common.data.id.CustomerId;
29 30 import org.thingsboard.server.common.data.id.EntityId;
... ... @@ -120,4 +121,9 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
120 121 public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
121 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 83 if (value != null) {
84 84 String strVal = value.toString();
85 85 // check number
86   - if (strVal.length() > 0 && NumberUtils.isParsable(strVal)) {
  86 + if (NumberUtils.isNumber(strVal)) {
87 87 try {
88 88 long longVal = Long.parseLong(strVal);
89 89 return Long.toString(longVal);
... ...
... ... @@ -116,16 +116,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
116 116 super.startExecutor();
117 117 if (!isInstall()) {
118 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 114 CREATE TABLE IF NOT EXISTS dashboard (
115 115 id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
116 116 created_time bigint NOT NULL,
117   - configuration varchar,
  117 + configuration varchar(10000000),
118 118 assigned_customers varchar(1000000),
119 119 search_text varchar(255),
120 120 tenant_id uuid,
... ...
... ... @@ -355,6 +355,62 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
355 355 }
356 356
357 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 414 public void testFindAlarmUsingAlarmDataQuery() throws ExecutionException, InterruptedException {
359 415 AssetId parentId = new AssetId(Uuids.timeBased());
360 416 AssetId parentId2 = new AssetId(Uuids.timeBased());
... ...
... ... @@ -827,10 +827,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
827 827 .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
828 828 }
829 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 831 Assert.assertEquals(deviceTemperatures, loadedTemperatures);
835 832
836 833 pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
... ... @@ -858,10 +855,6 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
858 855 entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList());
859 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 858 Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
866 859
867 860 deviceService.deleteDevicesByTenantId(tenantId);
... ...
... ... @@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
18 18
19 19 RUN apt-get update
20 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 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
23 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 24 RUN curl -L https://www.apache.org/dist/cassandra/KEYS | apt-key add -
... ...
... ... @@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
18 18
19 19 RUN apt-get update
20 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 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
23 23 ENV PG_MAJOR 11
24 24 RUN apt-get update
... ...
... ... @@ -1189,7 +1189,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1189 1189
1190 1190 public List<EntitySubtype> getDeviceTypes() {
1191 1191 return restTemplate.exchange(
1192   - baseURL + "/api/devices",
  1192 + baseURL + "/api/device/types",
1193 1193 HttpMethod.GET,
1194 1194 HttpEntity.EMPTY,
1195 1195 new ParameterizedTypeReference<List<EntitySubtype>>() {
... ...
... ... @@ -61,4 +61,12 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
61 61 configuration.setCredentials(new AnonymousCredentials());
62 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 53 private stateControllerHolder: StateControllerHolder,
54 54 private origEntityAliases: EntityAliases,
55 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 58 this.userFilters = {};
59 59 }
60 60
... ...
... ... @@ -15,7 +15,7 @@
15 15 ///
16 16
17 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 19 import { AuthService } from '../auth/auth.service';
20 20 import { select, Store } from '@ngrx/store';
21 21 import { AppState } from '../core.state';
... ... @@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum';
28 28 import { DialogService } from '@core/services/dialog.service';
29 29 import { TranslateService } from '@ngx-translate/core';
30 30 import { UtilsService } from '@core/services/utils.service';
  31 +import { isObject } from '@core/utils';
31 32
32 33 @Injectable({
33 34 providedIn: 'root'
... ... @@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
35 36 export class AuthGuard implements CanActivate, CanActivateChild {
36 37
37 38 constructor(private store: Store<AppState>,
  39 + private router: Router,
38 40 private authService: AuthService,
39 41 private dialogService: DialogService,
40 42 private utils: UtilsService,
... ... @@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild {
115 117 if (data.auth && data.auth.indexOf(authority) === -1) {
116 118 this.dialogService.forbidden();
117 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 128 } else {
119 129 return of(true);
120 130 }
... ...
... ... @@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
20 20 import { HttpClient } from '@angular/common/http';
21 21 import { PageLink } from '@shared/models/page/page-link';
22 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 24 import { WINDOW } from '@core/services/window.service';
25 25 import { NavigationEnd, Router } from '@angular/router';
26 26 import { filter, map, publishReplay, refCount } from 'rxjs/operators';
... ... @@ -122,6 +122,19 @@ export class DashboardService {
122 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 138 public getPublicDashboardLink(dashboard: DashboardInfo): string | null {
126 139 if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) {
127 140 const publicCustomers = dashboard.assignedCustomers
... ...
... ... @@ -223,6 +223,7 @@ export class MenuService {
223 223 name: 'home.home',
224 224 type: 'link',
225 225 path: '/home',
  226 + notExact: true,
226 227 icon: 'home'
227 228 },
228 229 {
... ... @@ -284,6 +285,13 @@ export class MenuService {
284 285 },
285 286 {
286 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 295 name: 'audit-log.audit-logs',
288 296 type: 'link',
289 297 path: '/auditLogs',
... ... @@ -402,6 +410,7 @@ export class MenuService {
402 410 name: 'home.home',
403 411 type: 'link',
404 412 path: '/home',
  413 + notExact: true,
405 414 icon: 'home'
406 415 },
407 416 {
... ...
... ... @@ -14,14 +14,23 @@
14 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 27 import { TooltipPosition } from '@angular/material/tooltip';
19 28 import { AliasInfo, IAliasController } from '@core/api/widget-api.models';
20 29 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
21 30 import { TranslateService } from '@ngx-translate/core';
22 31 import { Subscription } from 'rxjs';
23 32 import { BreakpointObserver } from '@angular/cdk/layout';
24   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  33 +import { ComponentPortal } from '@angular/cdk/portal';
25 34 import {
26 35 ALIASES_ENTITY_SELECT_PANEL_DATA,
27 36 AliasesEntitySelectPanelComponent,
... ... @@ -136,12 +145,12 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy {
136 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 156 private updateDisplayValue() {
... ...
... ... @@ -195,7 +195,8 @@
195 195 [length]="dataSource.total() | async"
196 196 [pageIndex]="pageLink.page"
197 197 [pageSize]="pageLink.pageSize"
198   - [pageSizeOptions]="[10, 20, 30]"></mat-paginator>
  198 + [pageSizeOptions]="[10, 20, 30]"
  199 + showFirstLastButtons></mat-paginator>
199 200 <ngx-hm-carousel fxFlex *ngIf="mode === 'widget' && widgetsList.length > 0"
200 201 #carousel
201 202 [(ngModel)]="widgetsCarouselIndex"
... ...
... ... @@ -19,9 +19,11 @@ import {
19 19 ChangeDetectionStrategy,
20 20 Component,
21 21 ElementRef,
  22 + Injector,
22 23 Input,
23 24 NgZone,
24 25 OnInit,
  26 + StaticProvider,
25 27 ViewChild,
26 28 ViewContainerRef
27 29 } from '@angular/core';
... ... @@ -62,7 +64,7 @@ import {
62 64 EditAttributeValuePanelComponent,
63 65 EditAttributeValuePanelData
64 66 } from './edit-attribute-value-panel.component';
65   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  67 +import { ComponentPortal } from '@angular/cdk/portal';
66 68 import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
67 69 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
68 70 import { DataKey, Datasource, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
... ... @@ -319,13 +321,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
319 321 overlayRef.backdropClick().subscribe(() => {
320 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 337 const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent,
330 338 this.viewContainerRef, injector));
331 339 componentRef.onDestroy(() => {
... ...
... ... @@ -49,10 +49,11 @@
49 49 <tb-states-component fxFlex.lt-md
50 50 [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
51 51 [dashboardCtrl]="this"
52   - [dashboardId]="dashboard.id ? dashboard.id.id : ''"
  52 + [dashboardId]="(!embedded && dashboard.id) ? dashboard.id.id : ''"
53 53 [isMobile]="isMobile"
54 54 [state]="dashboardCtx.state"
55 55 [currentState]="currentState"
  56 + [syncStateWithQueryParam]="syncStateWithQueryParam"
56 57 [states]="dashboardConfiguration.states">
57 58 </tb-states-component>
58 59 </div>
... ... @@ -78,7 +79,7 @@
78 79 (click)="isFullscreen = !isFullscreen">
79 80 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
80 81 </button>
81   - <button [fxShow]="isEdit || displayExport()" mat-icon-button
  82 + <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button
82 83 matTooltip="{{'dashboard.export' | translate}}"
83 84 matTooltipPosition="below"
84 85 (click)="exportDashboard($event)">
... ... @@ -118,7 +119,7 @@
118 119 (click)="openDashboardSettings($event)">
119 120 <mat-icon>settings</mat-icon>
120 121 </button>
121   - <tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && displayDashboardsSelect()"
  122 + <tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()"
122 123 [(ngModel)]="currentDashboardId"
123 124 (ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
124 125 [customerId]="currentCustomerId"
... ...
... ... @@ -124,6 +124,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
124 124 hideToolbar: boolean;
125 125
126 126 @Input()
  127 + syncStateWithQueryParam = true;
  128 +
  129 + @Input()
127 130 dashboard: Dashboard;
128 131 dashboardConfiguration: DashboardConfiguration;
129 132
... ... @@ -266,9 +269,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
266 269 this.rxSubscriptions.push(this.route.data.subscribe(
267 270 (data) => {
268 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 274 data.widgetEditMode = false;
271 275 data.singlePageMode = false;
  276 + } else {
  277 + data.currentDashboardId = this.route.snapshot.params.dashboardId;
272 278 }
273 279 this.init(data);
274 280 this.runChangeDetection();
... ... @@ -286,7 +292,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
286 292
287 293 this.reset();
288 294
289   - this.currentDashboardId = this.route.snapshot.params.dashboardId;
  295 + this.currentDashboardId = data.currentDashboardId;
290 296
291 297 if (this.route.snapshot.params.customerId) {
292 298 this.currentCustomerId = this.route.snapshot.params.customerId;
... ...
... ... @@ -23,6 +23,7 @@
23 23 [widgetLayouts]="{}"
24 24 [isEdit]="false"
25 25 [isMobile]="true"
  26 + [disableWidgetInteraction]="true"
26 27 [isEditActionEnabled]="false"
27 28 [isExportActionEnabled]="false"
28 29 [isRemoveActionEnabled]="false"
... ... @@ -34,6 +35,7 @@
34 35 [widgetLayouts]="{}"
35 36 [isEdit]="false"
36 37 [isMobile]="true"
  38 + [disableWidgetInteraction]="true"
37 39 [isEditActionEnabled]="false"
38 40 [isExportActionEnabled]="false"
39 41 [isRemoveActionEnabled]="false"
... ... @@ -45,6 +47,7 @@
45 47 [widgetLayouts]="{}"
46 48 [isEdit]="false"
47 49 [isMobile]="true"
  50 + [disableWidgetInteraction]="true"
48 51 [isEditActionEnabled]="false"
49 52 [isExportActionEnabled]="false"
50 53 [isRemoveActionEnabled]="false"
... ... @@ -56,6 +59,7 @@
56 59 [widgetLayouts]="{}"
57 60 [isEdit]="false"
58 61 [isMobile]="true"
  62 + [disableWidgetInteraction]="true"
59 63 [isEditActionEnabled]="false"
60 64 [isExportActionEnabled]="false"
61 65 [isRemoveActionEnabled]="false"
... ... @@ -67,6 +71,7 @@
67 71 [widgetLayouts]="{}"
68 72 [isEdit]="false"
69 73 [isMobile]="true"
  74 + [disableWidgetInteraction]="true"
70 75 [isEditActionEnabled]="false"
71 76 [isExportActionEnabled]="false"
72 77 [isRemoveActionEnabled]="false"
... ...
... ... @@ -53,6 +53,7 @@
53 53 [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
54 54 [isMobile]="isMobile"
55 55 [isMobileDisabled]="widgetEditMode"
  56 + [disableWidgetInteraction]="isEdit"
56 57 [isEditActionEnabled]="isEdit"
57 58 [isExportActionEnabled]="isEdit && !widgetEditMode"
58 59 [isRemoveActionEnabled]="isEdit && !widgetEditMode"
... ...
... ... @@ -88,6 +88,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon
88 88
89 89 currentState: string;
90 90
  91 + syncStateWithQueryParam: boolean;
  92 +
91 93 private rxSubscriptions = new Array<Subscription>();
92 94
93 95 private inited = false;
... ... @@ -99,18 +101,20 @@ export abstract class StateControllerComponent implements IStateControllerCompon
99 101 }
100 102
101 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 118 this.init();
115 119 this.inited = true;
116 120 }
... ... @@ -124,16 +128,18 @@ export abstract class StateControllerComponent implements IStateControllerCompon
124 128
125 129 protected updateStateParam(newState: string) {
126 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 145 public openRightLayout(): void {
... ...
... ... @@ -24,6 +24,7 @@ export interface IStateControllerComponent extends IStateController {
24 24 stateControllerInstanceId: string;
25 25 state: string;
26 26 currentState: string;
  27 + syncStateWithQueryParam: boolean;
27 28 isMobile: boolean;
28 29 states: {[id: string]: DashboardState };
29 30 dashboardId: string;
... ...
... ... @@ -54,6 +54,9 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
54 54 currentState: string;
55 55
56 56 @Input()
  57 + syncStateWithQueryParam: boolean;
  58 +
  59 + @Input()
57 60 isMobile: boolean;
58 61
59 62 stateControllerComponentRef: ComponentRef<IStateControllerComponent>;
... ... @@ -89,6 +92,8 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
89 92 this.stateControllerComponent.state = this.state;
90 93 } else if (propName === 'currentState') {
91 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 124 this.stateControllerComponent.stateControllerInstanceId = stateControllerInstanceId;
120 125 this.stateControllerComponent.state = this.state;
121 126 this.stateControllerComponent.currentState = this.currentState;
  127 + this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam;
122 128 this.stateControllerComponent.isMobile = this.isMobile;
123 129 this.stateControllerComponent.states = this.states;
124 130 this.stateControllerComponent.dashboardId = this.dashboardId;
... ...
... ... @@ -150,7 +150,7 @@
150 150 </button>
151 151 </div>
152 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 154 <tb-widget fxFlex
155 155 #widgetComponent
156 156 [dashboardWidget]="widget"
... ...
... ... @@ -128,6 +128,9 @@ div.tb-widget {
128 128 }
129 129
130 130 .tb-widget-content {
  131 + &.tb-no-interaction {
  132 + pointer-events: none;
  133 + }
131 134 tb-widget {
132 135 position: relative;
133 136 width: 100%;
... ...
... ... @@ -114,6 +114,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
114 114 isRemoveActionEnabled: boolean;
115 115
116 116 @Input()
  117 + disableWidgetInteraction = false;
  118 +
  119 + @Input()
117 120 dashboardStyle: {[klass: string]: any};
118 121
119 122 @Input()
... ...
... ... @@ -238,7 +238,8 @@
238 238 [length]="dataSource.total() | async"
239 239 [pageIndex]="pageLink.page"
240 240 [pageSize]="pageLink.pageSize"
241   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  241 + [pageSizeOptions]="pageSizeOptions"
  242 + showFirstLastButtons></mat-paginator>
242 243 </div>
243 244 </div>
244 245 </mat-drawer-content>
... ...
... ... @@ -44,7 +44,7 @@ import { EntityAction } from '@home/models/entity/entity-component.models';
44 44 import { Subscription } from 'rxjs';
45 45 import { MatTab, MatTabGroup } from '@angular/material/tabs';
46 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 49 @Component({
50 50 selector: 'tb-entity-details-panel',
... ... @@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
280 280
281 281 saveEntity() {
282 282 if (this.detailsForm.valid) {
283   - const editingEntity = {...this.editingEntity, ...this.entityComponent.entityFormValue()};
  283 + const editingEntity = mergeDeep(this.editingEntity, this.entityComponent.entityFormValue());
284 284 this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
285 285 (entity) => {
286 286 this.entity = entity;
... ...
... ... @@ -14,7 +14,16 @@
14 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 27 import { TooltipPosition } from '@angular/material/tooltip';
19 28 import { IAliasController } from '@core/api/widget-api.models';
20 29 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
... ... @@ -28,7 +37,7 @@ import {
28 37 FiltersEditPanelComponent,
29 38 FiltersEditPanelData
30 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 41 import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component';
33 42 import { MatDialog } from '@angular/material/dialog';
34 43
... ... @@ -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 173 private updateFiltersInfo() {
... ...
... ... @@ -67,15 +67,13 @@
67 67 </mat-form-field>
68 68 <section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value">
69 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 77 </ng-template>
80 78 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
81 79 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
... ... @@ -122,26 +120,24 @@
122 120 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
123 121 widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
124 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 134 </ng-template>
139 135 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ?
140 136 widgetActionFormGroup.get('type').value : ''">
141 137 <mat-checkbox formControlName="openInSeparateDialog">
142 138 {{ 'widget-action.open-in-separate-dialog' | translate }}
143 139 </mat-checkbox>
144   - <section fxLayout="column" *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value">
  140 + <section *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value">
145 141 <mat-form-field class="mat-block">
146 142 <mat-label translate>widget-action.dialog-title</mat-label>
147 143 <input matInput formControlName="dialogTitle">
... ...
... ... @@ -27,7 +27,8 @@
27 27 </mat-progress-bar>
28 28 <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
29 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 32 </div>
32 33 <div mat-dialog-actions fxLayoutAlign="end center">
33 34 <button mat-button color="primary"
... ...
... ... @@ -14,11 +14,22 @@
14 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 29 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 30 import { DOCUMENT } from '@angular/common';
20 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 33 import { MediaBreakpoints } from '@shared/models/constants';
23 34 import { BreakpointObserver } from '@angular/cdk/layout';
24 35 import { WINDOW } from '@core/services/window.service';
... ... @@ -140,12 +151,12 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
140 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 162 registerOnChange(fn: any): void {
... ...
... ... @@ -140,6 +140,7 @@
140 140 [length]="alarmsDatasource.total() | async"
141 141 [pageIndex]="pageLink.page"
142 142 [pageSize]="pageLink.pageSize"
143   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  143 + [pageSizeOptions]="pageSizeOptions"
  144 + showFirstLastButtons></mat-paginator>
144 145 </div>
145 146 </div>
... ...
... ... @@ -19,9 +19,11 @@ import {
19 19 Component,
20 20 ElementRef,
21 21 EventEmitter,
  22 + Injector,
22 23 Input,
23 24 NgZone,
24 25 OnInit,
  26 + StaticProvider,
25 27 ViewChild,
26 28 ViewContainerRef
27 29 } from '@angular/core';
... ... @@ -64,7 +66,7 @@ import {
64 66 widthStyle
65 67 } from '@home/components/widget/lib/table-widget.models';
66 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 70 import {
69 71 DISPLAY_COLUMNS_PANEL_DATA,
70 72 DisplayColumnsPanelComponent,
... ... @@ -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 477 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
470 478 this.viewContainerRef, injector));
471 479 this.ctx.detectChanges();
... ... @@ -492,15 +500,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
492 500 overlayRef.backdropClick().subscribe(() => {
493 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 518 const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent,
505 519 this.viewContainerRef, injector));
506 520 componentRef.onDestroy(() => {
... ...
... ... @@ -18,9 +18,11 @@ import {
18 18 Component,
19 19 Inject,
20 20 InjectionToken,
  21 + Injector,
21 22 Input,
22 23 OnDestroy,
23 24 OnInit,
  25 + StaticProvider,
24 26 ViewChild,
25 27 ViewContainerRef,
26 28 ViewEncapsulation
... ... @@ -41,7 +43,7 @@ import {
41 43 import { KeyValue } from '@angular/common';
42 44 import * as _moment from 'moment';
43 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 47 import { MatSelect } from '@angular/material/select';
46 48 import { Subscription } from 'rxjs';
47 49 import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.models';
... ... @@ -142,18 +144,24 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements
142 144 overlayRef.backdropClick().subscribe(() => {
143 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 165 overlayRef.attach(new ComponentPortal(DateRangeNavigatorPanelComponent,
158 166 this.viewContainerRef, injector));
159 167 this.ctx.detectChanges();
... ...
... ... @@ -99,6 +99,7 @@
99 99 [length]="entityDatasource.total() | async"
100 100 [pageIndex]="pageLink.page"
101 101 [pageSize]="pageLink.pageSize"
102   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  102 + [pageSizeOptions]="pageSizeOptions"
  103 + showFirstLastButtons></mat-paginator>
103 104 </div>
104 105 </div>
... ...
... ... @@ -18,9 +18,11 @@ import {
18 18 AfterViewInit,
19 19 Component,
20 20 ElementRef,
  21 + Injector,
21 22 Input,
22 23 NgZone,
23 24 OnInit,
  25 + StaticProvider,
24 26 ViewChild,
25 27 ViewContainerRef
26 28 } from '@angular/core';
... ... @@ -70,7 +72,7 @@ import {
70 72 widthStyle
71 73 } from '@home/components/widget/lib/table-widget.models';
72 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 76 import {
75 77 DISPLAY_COLUMNS_PANEL_DATA,
76 78 DisplayColumnsPanelComponent,
... ... @@ -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 444 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
437 445 this.viewContainerRef, injector));
438 446 this.ctx.detectChanges();
... ...
... ... @@ -175,7 +175,7 @@ export class TbFlot {
175 175 autoHighlight: this.tooltipIndividual === true,
176 176 markings: []
177 177 },
178   - selection : { mode : ctx.isMobile ? null : 'x' },
  178 + selection : { mode : 'x' },
179 179 legend : {
180 180 show: false
181 181 }
... ... @@ -702,7 +702,7 @@ export class TbFlot {
702 702 }
703 703
704 704 public checkMouseEvents() {
705   - const enabled = !this.ctx.isMobile && !this.ctx.isEdit;
  705 + const enabled = !this.ctx.isEdit;
706 706 if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) {
707 707 this.mouseEventsEnabled = enabled;
708 708 if (this.$element) {
... ...
... ... @@ -626,10 +626,14 @@ export default abstract class LeafletMap {
626 626 }
627 627 this.points = new FeatureGroup();
628 628 }
  629 + let pointColor = this.options.pointColor;
629 630 for (const pointsList of pointsData) {
630 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 635 const point = L.circleMarker(this.convertPosition(data), {
632   - color: this.options.pointColor,
  636 + color: pointColor,
633 637 radius: this.options.pointSize
634 638 });
635 639 if (!this.options.pointTooltipOnRightPanel) {
... ...
... ... @@ -201,6 +201,8 @@ export type TripAnimationSettings = {
201 201 pointAsAnchorFunction: GenericFunction;
202 202 tooltipFunction: GenericFunction;
203 203 labelFunction: GenericFunction;
  204 + useColorPointFunction: boolean;
  205 + colorPointFunction: GenericFunction;
204 206 };
205 207
206 208 export type actionsHandler = ($event: Event, datasource: Datasource) => void;
... ...
... ... @@ -301,6 +301,7 @@ export class MapWidgetController implements MapWidgetInterface {
301 301 labelFunction: parseFunction(settings.labelFunction, functionParams),
302 302 tooltipFunction: parseFunction(settings.tooltipFunction, functionParams),
303 303 colorFunction: parseFunction(settings.colorFunction, functionParams),
  304 + colorPointFunction: parseFunction(settings.colorPointFunction, functionParams),
304 305 polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams),
305 306 polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams),
306 307 markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']),
... ...
... ... @@ -871,6 +871,15 @@ export const pointSchema =
871 871 title: 'Point color',
872 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 883 pointSize: {
875 884 title: 'Point size (px)',
876 885 type: 'number',
... ... @@ -899,6 +908,11 @@ export const pointSchema =
899 908 key: 'pointColor',
900 909 type: 'color'
901 910 },
  911 + 'useColorPointFunction',
  912 + {
  913 + key: 'colorPointFunction',
  914 + type: 'javascript'
  915 + },
902 916 'pointSize',
903 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 104 [length]="source.timeseriesDatasource.total() | async"
105 105 [pageIndex]="source.pageLink.page"
106 106 [pageSize]="source.pageLink.pageSize"
107   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  107 + [pageSizeOptions]="pageSizeOptions"
  108 + showFirstLastButtons></mat-paginator>
108 109 </mat-tab>
109 110 </mat-tab-group>
110 111 </div>
... ...
... ... @@ -210,7 +210,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
210 210 this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
211 211 this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false;
212 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 215 const pageSize = this.settings.defaultPageSize;
216 216 if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
... ...
... ... @@ -112,6 +112,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
112 112 this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']);
113 113 this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']);
114 114 this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']);
  115 + this.settings.colorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']);
115 116 this.normalizationStep = this.settings.normalizationStep;
116 117 const subscription = this.ctx.defaultSubscription;
117 118 subscription.callbacks.onDataUpdated = () => {
... ...
... ... @@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen
35 35 import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component';
36 36 import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
37 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 41 @NgModule({
40 42 declarations:
... ... @@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
50 52 MultipleInputWidgetComponent,
51 53 TripAnimationComponent,
52 54 PhotoCameraInputWidgetComponent,
53   - GatewayFormComponent
  55 + GatewayFormComponent,
  56 + NavigationCardsWidgetComponent,
  57 + NavigationCardWidgetComponent
54 58 ],
55 59 imports: [
56 60 CommonModule,
... ... @@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
68 72 MultipleInputWidgetComponent,
69 73 TripAnimationComponent,
70 74 PhotoCameraInputWidgetComponent,
71   - GatewayFormComponent
  75 + GatewayFormComponent,
  76 + NavigationCardsWidgetComponent,
  77 + NavigationCardWidgetComponent
72 78 ],
73 79 providers: [
74 80 CustomDialogService,
... ...
... ... @@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
32 32 import { OAuth2Service } from '@core/http/oauth2.service';
33 33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module';
34 34 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
  35 +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
35 36
36 37 @Injectable()
37 38 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
... ... @@ -48,7 +49,7 @@ const routes: Routes = [
48 49 {
49 50 path: 'settings',
50 51 data: {
51   - auth: [Authority.SYS_ADMIN],
  52 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
52 53 breadcrumb: {
53 54 label: 'admin.system-settings',
54 55 icon: 'settings'
... ... @@ -57,8 +58,13 @@ const routes: Routes = [
57 58 children: [
58 59 {
59 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 70 path: 'general',
... ... @@ -127,6 +133,19 @@ const routes: Routes = [
127 133 resolve: {
128 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 26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
27 27 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
28 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 31 @NgModule({
31 32 declarations:
... ... @@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial
35 36 SmsProviderComponent,
36 37 SendTestSmsDialogComponent,
37 38 SecuritySettingsComponent,
38   - OAuth2SettingsComponent
  39 + OAuth2SettingsComponent,
  40 + HomeSettingsComponent
39 41 ],
40 42 imports: [
41 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 72 <mat-label translate>customer.description</mat-label>
73 73 <textarea matInput formControlName="description" rows="2"></textarea>
74 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 90 </div>
76 91 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
77 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 23 import { TranslateService } from '@ngx-translate/core';
24 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 28 @Component({
28 29 selector: 'tb-customer',
29   - templateUrl: './customer.component.html'
  30 + templateUrl: './customer.component.html',
  31 + styleUrls: ['./customer.component.scss']
30 32 })
31 33 export class CustomerComponent extends ContactBasedComponent<Customer> {
32 34
... ... @@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
54 56 title: [entity ? entity.title : '', [Validators.required]],
55 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 70 this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic;
66 71 this.entityForm.patchValue({title: entity.title});
67 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 80 onCustomerIdCopied(event) {
... ...
... ... @@ -14,11 +14,25 @@
14 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 20 import { HomeLinksComponent } from './home-links.component';
21 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 37 const routes: Routes = [
24 38 {
... ... @@ -31,12 +45,18 @@ const routes: Routes = [
31 45 label: 'home.home',
32 46 icon: 'home'
33 47 }
  48 + },
  49 + resolve: {
  50 + homeDashboard: HomeDashboardResolver
34 51 }
35 52 }
36 53 ];
37 54
38 55 @NgModule({
39 56 imports: [RouterModule.forChild(routes)],
40   - exports: [RouterModule]
  57 + exports: [RouterModule],
  58 + providers: [
  59 + HomeDashboardResolver
  60 + ]
41 61 })
42 62 export class HomeLinksRoutingModule { }
... ...
... ... @@ -15,23 +15,26 @@
15 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 15 */
16 16 @import '../../../../../scss/constants';
17 17
  18 +:host {
  19 + width: 100%;
  20 + height: 100%;
  21 +}
  22 +
18 23 :host ::ng-deep {
19 24 .tb-home-links {
20 25 .mat-headline {
... ...
... ... @@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service';
19 19 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
20 20 import { MediaBreakpoints } from '@shared/models/constants';
21 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 25 @Component({
24 26 selector: 'tb-home-links',
... ... @@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit {
31 33
32 34 cols = 2;
33 35
  36 + homeDashboard: HomeDashboard = this.route.snapshot.data.homeDashboard;
  37 +
34 38 constructor(private menuService: MenuService,
35   - public breakpointObserver: BreakpointObserver) {
  39 + public breakpointObserver: BreakpointObserver,
  40 + private route: ActivatedRoute) {
36 41 }
37 42
38 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 52 private updateColumnCount() {
... ...
... ... @@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common';
20 20 import { HomeLinksRoutingModule } from './home-links-routing.module';
21 21 import { HomeLinksComponent } from './home-links.component';
22 22 import { SharedModule } from '@app/shared/shared.module';
  23 +import { HomeComponentsModule } from '@home/components/home-components.module';
23 24
24 25 @NgModule({
25 26 declarations:
... ... @@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module';
29 30 imports: [
30 31 CommonModule,
31 32 SharedModule,
  33 + HomeComponentsModule,
32 34 HomeLinksRoutingModule
33 35 ]
34 36 })
... ...
... ... @@ -63,6 +63,20 @@
63 63 </mat-option>
64 64 </mat-select>
65 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 80 <div fxLayout="row" style="padding-bottom: 16px;">
67 81 <button mat-button mat-raised-button color="primary"
68 82 type="button"
... ...
... ... @@ -38,5 +38,21 @@
38 38 font-size: 16px;
39 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 32 import { DialogService } from '@core/services/dialog.service';
33 33 import { AuthService } from '@core/auth/auth.service';
34 34 import { ActivatedRoute } from '@angular/router';
  35 +import { isDefinedAndNotNull } from '@core/utils';
35 36
36 37 @Component({
37 38 selector: 'tb-profile',
... ... @@ -66,7 +67,9 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
66 67 email: ['', [Validators.required, Validators.email]],
67 68 firstName: [''],
68 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 79 this.user.additionalInfo = {};
77 80 }
78 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 84 this.userService.saveUser(this.user).subscribe(
80 85 (user) => {
81 86 this.userLoaded(user);
... ... @@ -106,12 +111,23 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
106 111 this.user = user;
107 112 this.profile.reset(user);
108 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 126 lang = this.translate.currentLang;
113 127 }
114 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 133 confirmForm(): FormGroup {
... ...
... ... @@ -60,6 +60,21 @@
60 60 <mat-label translate>tenant.description</mat-label>
61 61 <textarea matInput formControlName="description" rows="2"></textarea>
62 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 78 </div>
64 79 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
65 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 23 import { TranslateService } from '@ngx-translate/core';
24 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 28 @Component({
28 29 selector: 'tb-tenant',
29 30 templateUrl: './tenant.component.html',
30   - styleUrls: []
  31 + styleUrls: ['./tenant.component.scss']
31 32 })
32 33 export class TenantComponent extends ContactBasedComponent<TenantInfo> {
33 34
... ... @@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
54 55 tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]],
55 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 69 this.entityForm.patchValue({title: entity.title});
66 70 this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId});
67 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 79 updateFormState() {
... ...
... ... @@ -95,6 +95,20 @@
95 95 {{ 'user.always-fullscreen' | translate }}
96 96 </mat-checkbox>
97 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 112 </section>
99 113 </div>
100 114 </fieldset>
... ...
... ... @@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model';
23 23 import { selectAuth } from '@core/auth/auth.selectors';
24 24 import { map } from 'rxjs/operators';
25 25 import { Authority } from '@shared/models/authority.enum';
26   -import { isUndefined } from '@core/utils';
  26 +import { isDefinedAndNotNull, isUndefined } from '@core/utils';
27 27 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
28 28
29 29 @Component({
... ... @@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent<User> {
74 74 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
75 75 defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
76 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 92 {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
90 93 this.entityForm.patchValue({additionalInfo:
91 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 35 [isEditActionEnabled]="true"
36 36 [isExportActionEnabled]="true"
37 37 [isRemoveActionEnabled]="!isReadOnly"
  38 + [disableWidgetInteraction]="true"
38 39 [callbacks]="dashboardCallbacks"></tb-dashboard>
39 40 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
40 41 </tb-footer-fab-buttons>
... ...
... ... @@ -14,7 +14,17 @@
14 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 28 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 29 import { Observable, of } from 'rxjs';
20 30 import { PageLink } from '@shared/models/page/page-link';
... ... @@ -32,7 +42,7 @@ import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef
32 42 import { BreakpointObserver } from '@angular/cdk/layout';
33 43 import { DOCUMENT } from '@angular/common';
34 44 import { WINDOW } from '@core/services/window.service';
35   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  45 +import { ComponentPortal } from '@angular/cdk/portal';
36 46 import {
37 47 DASHBOARD_SELECT_PANEL_DATA,
38 48 DashboardSelectPanelComponent,
... ... @@ -186,12 +196,12 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
186 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 207 private updateView() {
... ...
... ... @@ -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 36 return (
32 37 <FormControl component='fieldset'
33 38 className={this.props.form.htmlClass}
34 39 disabled={this.props.form.readonly}>
35 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 42 this.props.onChangeValidate(e);
38 43 }}>
39 44 {items}
... ...
... ... @@ -22,6 +22,7 @@ import {
22 22 KeyLabelItem
23 23 } from '@shared/components/json-form/react/json-form.models';
24 24 import { Mode } from 'rc-select/lib/interface';
  25 +import { deepClone } from '@core/utils';
25 26
26 27 interface ThingsboardRcSelectState extends JsonFormFieldState {
27 28 currentValue: KeyLabelItem | KeyLabelItem[];
... ... @@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
151 152 labelClass += ' tb-focused';
152 153 }
153 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 165 const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}};
... ... @@ -176,12 +181,13 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
176 181 maxTagTextLength={this.props.form.maxTagTextLength}
177 182 disabled={this.props.form.readonly}
178 183 optionLabelProp='children'
179   - value={this.state.currentValue}
  184 + value={value}
180 185 labelInValue={true}
181 186 onSelect={this.onSelect}
182 187 onDeselect={this.onDeselect}
183 188 onFocus={this.onFocus}
184 189 onBlur={this.onBlur}
  190 + placeholder={this.props.form.placeholder}
185 191 style={this.props.form.style || {width: '100%'}}>
186 192 {options}
187 193 </Select>
... ...
... ... @@ -14,7 +14,18 @@
14 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 29 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 30 import { TranslateService } from '@ngx-translate/core';
20 31 import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
... ... @@ -32,7 +43,7 @@ import {
32 43 TimewindowPanelComponent,
33 44 TimewindowPanelData
34 45 } from '@shared/components/time/timewindow-panel.component';
35   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  46 +import { ComponentPortal } from '@angular/cdk/portal';
36 47 import { MediaBreakpoints } from '@shared/models/constants';
37 48 import { BreakpointObserver } from '@angular/cdk/layout';
38 49 import { WINDOW } from '@core/services/window.service';
... ... @@ -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 251 registerOnChange(fn: any): void {
... ...
... ... @@ -20,9 +20,11 @@ import {
20 20 Directive,
21 21 ElementRef, HostBinding,
22 22 Inject,
  23 + Injector,
23 24 Input,
24 25 NgZone,
25 26 OnDestroy, Optional,
  27 + StaticProvider,
26 28 ViewChild,
27 29 ViewContainerRef
28 30 } from '@angular/core';
... ... @@ -34,7 +36,6 @@ import { BreakpointObserver } from '@angular/cdk/layout';
34 36 import { MediaBreakpoints } from '@shared/models/constants';
35 37 import { MatButton } from '@angular/material/button';
36 38 import Timeout = NodeJS.Timeout;
37   -import { PortalInjector } from '@angular/cdk/portal';
38 39
39 40 @Directive({
40 41 selector: '[tb-toast]'
... ... @@ -138,10 +139,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
138 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 146 this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector);
146 147 this.cd.detectChanges();
147 148
... ...
... ... @@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo {
106 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 118 export function isPublicDashboard(dashboard: DashboardInfo): boolean {
110 119 if (dashboard && dashboard.assignedCustomers) {
111 120 return dashboard.assignedCustomers
... ...
... ... @@ -54,7 +54,10 @@
54 54 "share-via": "Sdílet přes {{provider}}",
55 55 "continue": "Pokračovat",
56 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 62 "aggregation": {
60 63 "aggregation": "Agregace",
... ... @@ -77,6 +80,8 @@
77 80 "test-mail-sent": "Testovací zpráva byla úspěšně odeslána!",
78 81 "base-url": "Základní URL",
79 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 85 "mail-from": "Email od",
81 86 "mail-from-required": "Hodnota Email od je povinná.",
82 87 "smtp-protocol": "SMTP protokol",
... ... @@ -99,6 +104,33 @@
99 104 "proxy-user": "Uživatel proxy",
100 105 "proxy-password": "Heslo proxy",
101 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 134 "security-settings": "Bezpečnostní nastavení",
103 135 "password-policy": "Politika hesel",
104 136 "minimum-password-length": "Minimální délka hesla",
... ... @@ -119,8 +151,74 @@
119 151 "general-policy": "Obecná politika",
120 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 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 222 "alarm": {
125 223 "alarm": "Alarm",
126 224 "alarms": "Alarmy",
... ... @@ -128,6 +226,8 @@
128 226 "no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.",
129 227 "alarm-required": "Alarm je povinný",
130 228 "alarm-status": "Stav alarmu",
  229 + "alarm-status-list": "Seznam stavů alarmu",
  230 + "any-status": "Všechny stavy",
131 231 "search-status": {
132 232 "ANY": "Všechny",
133 233 "ACTIVE": "Aktivní",
... ... @@ -154,6 +254,8 @@
154 254 "end-time": "Datum ukončení",
155 255 "ack-time": "Datum přijetí",
156 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 259 "severity-critical": "Kritická",
158 260 "severity-major": "Vysoká",
159 261 "severity-minor": "Nízká",
... ... @@ -176,12 +278,16 @@
176 278 "clear-alarm-title": "Odstranit alarm",
177 279 "clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?",
178 280 "alarm-status-filter": "Filtr stavu alarmu",
  281 + "alarm-filter": "Filtr alarmu",
179 282 "max-count-load": "Maximální počet nahraných alarmů (0 - neomezeně)",
180 283 "max-count-load-required": "Maximální počet nahraných alarmů je povinný.",
181 284 "max-count-load-error-min": "Minimální hodnota je 0.",
182 285 "fetch-size": "Velikost dávky",
183 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 292 "alias": {
187 293 "add": "Přidat alias",
... ... @@ -211,6 +317,7 @@
211 317 "filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
212 318 "filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity",
213 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 321 "entity-filter": "Filtr entity",
215 322 "resolve-multiple": "Použít jako více entit",
216 323 "filter-type": "Typ filtru",
... ... @@ -325,6 +432,59 @@
325 432 "no-attributes-text": "Žádné atributy nebyly nalezeny",
326 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 488 "audit-log": {
329 489 "audit": "Audit",
330 490 "audit-logs": "Záznamy auditu",
... ... @@ -363,7 +523,13 @@
363 523 "action-data": "Data akce",
364 524 "failure-details": "Detail chyby",
365 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 534 "confirm-on-exit": {
369 535 "message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?",
... ... @@ -549,6 +715,7 @@
549 715 "title-color": "Barva názvu",
550 716 "display-dashboards-selection": "Zobrazit výběr dashboardů",
551 717 "display-entities-selection": "Zobrazit výběr entit",
  718 + "display-filters": "Zobrazit filtry",
552 719 "display-dashboard-timewindow": "Zobrazit časové okno",
553 720 "display-dashboard-export": "Zobrazit export",
554 721 "import": "Importovat dashboard",
... ... @@ -615,6 +782,7 @@
615 782 "alarm": "Pole alarmu",
616 783 "timeseries-required": "Časové řady entity jsou povinné.",
617 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 786 "maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }",
619 787 "alarm-fields-required": "Pole alarmu jsou povinná.",
620 788 "function-types": "Typy funkcí",
... ... @@ -706,6 +874,12 @@
706 874 "access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.",
707 875 "rsa-key": "RSA veřejný klíč",
708 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 883 "secret": "Heslo",
710 884 "secret-required": "Heslo je povinné.",
711 885 "device-type": "Typ zařízení",
... ... @@ -724,19 +898,183 @@
724 898 "details": "Detail",
725 899 "copyId": "Kopírovat Id zařízení",
726 900 "copyAccessToken": "Kopírovat přístupový token",
  901 + "copy-mqtt-authentication": "Kopírovat přístupové údaje MQTT",
727 902 "idCopiedMessage": "Id zařízení bylo zkopírováno do schránky",
728 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 905 "assignedToCustomer": "Přiřazeno zákazníkovi",
730 906 "unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení",
731 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 908 "is-gateway": "Je bránou",
  909 + "overwrite-activity-time": "Přepsat čas aktivity připojeného zařízení",
733 910 "public": "Veřejné",
734 911 "device-public": "Zařízení je veřejné",
735 912 "select-device": "Vybrat zařízení",
736 913 "import": "Importovat zařízení",
737 914 "device-file": "Soubor zařízení",
738 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 1079 "dialog": {
742 1080 "close": "Zavřít dialog"
... ... @@ -757,7 +1095,7 @@
757 1095 "entity-alias": "Alias entity",
758 1096 "unable-delete-entity-alias-title": "Alias entity nebylo možné smazat",
759 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 1099 "missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.",
762 1100 "configure-alias": "Konfigurovat '{{alias}}' alias",
763 1101 "alias": "Alias",
... ... @@ -794,6 +1132,10 @@
794 1132 "type-devices": "Zařízení",
795 1133 "list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }",
796 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 1139 "type-asset": "Aktivum",
798 1140 "type-assets": "Aktiva",
799 1141 "list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }",
... ... @@ -814,6 +1156,10 @@
814 1156 "type-tenants": "Tenanti",
815 1157 "list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }",
816 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 1163 "type-customer": "Zákazník",
818 1164 "type-customers": "Zákazníci",
819 1165 "list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }",
... ... @@ -840,6 +1186,8 @@
840 1186 "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'",
841 1187 "type-current-customer": "Stávající zákazník",
842 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 1191 "search": "Vyhledat entity",
844 1192 "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno",
845 1193 "entity-name": "Název entity",
... ... @@ -847,7 +1195,8 @@
847 1195 "details": "Detail entity",
848 1196 "no-entities-prompt": "Žádné entity nebyly nalezeny",
849 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 1201 "entity-field": {
853 1202 "created-time": "Datum vytvoření",
... ... @@ -1048,7 +1397,7 @@
1048 1397 "anonymous": "Anonymní",
1049 1398 "basic": "Základní",
1050 1399 "pem": "PEM",
1051   - "ca-cert": "soubor CA certifikátu *",
  1400 + "ca-cert": "Soubor CA certifikátu *",
1052 1401 "private-key": "Soubor privátního klíče *",
1053 1402 "cert": "Soubor certifikátu *",
1054 1403 "no-file": "Žádný soubor nebyl vybrán.",
... ... @@ -1154,6 +1503,93 @@
1154 1503 "file": "Soubor rozšíření",
1155 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 1593 "fullscreen": {
1158 1594 "expand": "Rozšířit do režimu celé obrazovky",
1159 1595 "exit": "Ukončit režim celé obrazovky",
... ... @@ -1286,6 +1722,7 @@
1286 1722 "entity-field": "Pole entity",
1287 1723 "access-token": "Přístupový token",
1288 1724 "isgateway": "Je bránou",
  1725 + "activity-time-from-gateway-device": "Čas aktivity ze zařízení brány",
1289 1726 "description": "Popis"
1290 1727 },
1291 1728 "stepper-text":{
... ... @@ -1329,6 +1766,7 @@
1329 1766 "legend": {
1330 1767 "direction": "Směr legendy",
1331 1768 "position": "Pozice legendy",
  1769 + "sort-legend": "Setřídit datové klíče v legendě",
1332 1770 "show-max": "Zobrazit max hodnotu",
1333 1771 "show-min": "Zobrazit min hodnotu",
1334 1772 "show-avg": "Zobrazit průměrnou hodnotu",
... ... @@ -1525,6 +1963,12 @@
1525 1963 "help": "Nápověda",
1526 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 1972 "queue": {
1529 1973 "select_name": "Vybrat název fronty",
1530 1974 "name": "Název fronty",
... ... @@ -1563,6 +2007,87 @@
1563 2007 "isolated-tb-core-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta",
1564 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 2091 "timeinterval": {
1567 2092 "seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }",
1568 2093 "minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }",
... ... @@ -1574,8 +2099,14 @@
1574 2099 "seconds": "Vteřiny",
1575 2100 "advanced": "Rozšířené"
1576 2101 },
  2102 + "timeunit": {
  2103 + "seconds": "Vteřiny",
  2104 + "minutes": "Minuty",
  2105 + "hours": "Hodiny",
  2106 + "days": "Dny"
  2107 + },
1577 2108 "timewindow": {
1578   - "days": "{ days, plural, 1 { den } other {# days } }",
  2109 + "days": "{ days, plural, 1 { den } other {# d } }",
1579 2110 "hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }",
1580 2111 "minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }",
1581 2112 "seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }",
... ... @@ -1694,6 +2225,7 @@
1694 2225 "type": "Typ widgetu",
1695 2226 "resources": "Zdroje",
1696 2227 "resource-url": "JavaScript/CSS URL",
  2228 + "resource-is-module": "Je modulem",
1697 2229 "remove-resource": "Odebrat zdroj",
1698 2230 "add-resource": "Přidat zdroj",
1699 2231 "html": "HTML",
... ... @@ -1711,7 +2243,10 @@
1711 2243 "widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!",
1712 2244 "add": "Přidat widget",
1713 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 2251 "widget-action": {
1717 2252 "header-button": "Tlačítko hlavičky widgetu",
... ... @@ -1724,7 +2259,14 @@
1724 2259 "target-dashboard-state-required": "Cílový stav dashboardu je povinný",
1725 2260 "set-entity-from-widget": "Nastavit entitu z widgetu",
1726 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 2271 "widgets-bundle": {
1730 2272 "current": "Vybraná kategorie",
... ... @@ -1891,8 +2433,11 @@
1891 2433 "entity-coordinate-required": "Obě pole, zeměpisná šířka i zeměpisná délka, jsou povinná",
1892 2434 "entity-timeseries-required": "Časové řady entity jsou povinné",
1893 2435 "get-location": "Získat aktuální polohu",
  2436 + "invalid-date": "Neplatné datum",
1894 2437 "latitude": "Zeměpisná šířka",
1895 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 2441 "not-allowed-entity": "Vybraná entita nemůže mít sdílené atributy",
1897 2442 "no-attribute-selected": "Není vybrán žádný atribut",
1898 2443 "no-datakey-selected": "Není vybrán žádný datový klíč",
... ... @@ -1900,7 +2445,10 @@
1900 2445 "no-entity-selected": "Není vybrána žádná entita",
1901 2446 "no-image": "Žádný obrázek",
1902 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 2452 "no-timeseries-selected": "Nejsou vybrány žádné časové řady",
1905 2453 "secret-key": "Tajný klíč",
1906 2454 "secret-key-required": "Tajný klíč je povinný",
... ...
... ... @@ -74,6 +74,7 @@
74 74 "admin": {
75 75 "general": "General",
76 76 "general-settings": "General Settings",
  77 + "home-settings": "Home Settings",
77 78 "outgoing-mail": "Mail Server",
78 79 "outgoing-mail-settings": "Outgoing Mail Server Settings",
79 80 "system-settings": "System Settings",
... ... @@ -764,7 +765,9 @@
764 765 "select-state": "Select target state",
765 766 "state-controller": "State controller",
766 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 772 "datakey": {
770 773 "settings": "Settings",
... ...