Commit 9b4309c61a5349a7ea6c7bf45a5d69ccc34a524a
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; | ... | ... |
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
... | ... | @@ -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 | +} | ... | ... |
application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java
0 → 100644
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" | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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 {# dnů } }", | |
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", | ... | ... |