Commit 9b4309c61a5349a7ea6c7bf45a5d69ccc34a524a

Authored by Vladyslav_Prykhodko
2 parents 79814877 b6299623

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

Showing 102 changed files with 3121 additions and 435 deletions
... ... @@ -26,22 +26,6 @@
26 26 }
27 27 },
28 28 {
29   - "alias": "basic_timeseries",
30   - "name": "Timeseries - Flot",
31   - "descriptor": {
32   - "type": "timeseries",
33   - "sizeX": 8,
34   - "sizeY": 5,
35   - "resources": [],
36   - "templateHtml": "",
37   - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
38   - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
39   - "settingsSchema": "{}",
40   - "dataKeySettingsSchema": "{}",
41   - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
42   - }
43   - },
44   - {
45 29 "alias": "doughnut_chart_js",
46 30 "name": "Doughnut - Chart.js",
47 31 "descriptor": {
... ... @@ -71,7 +55,7 @@
71 55 "resources": [],
72 56 "templateHtml": "",
73 57 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n",
74   - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n",
  58 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n",
75 59 "settingsSchema": "{}\n",
76 60 "dataKeySettingsSchema": "{}\n",
77 61 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
... ... @@ -138,8 +122,8 @@
138 122 }
139 123 },
140 124 {
141   - "alias": "timeseries_bars_flot",
142   - "name": "Timeseries Bars - Flot",
  125 + "alias": "state_chart",
  126 + "name": "State Chart",
143 127 "descriptor": {
144 128 "type": "timeseries",
145 129 "sizeX": 8,
... ... @@ -147,15 +131,15 @@
147 131 "resources": [],
148 132 "templateHtml": "",
149 133 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
150   - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  134 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
151 135 "settingsSchema": "{}",
152 136 "dataKeySettingsSchema": "{}",
153   - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
  137 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
154 138 }
155 139 },
156 140 {
157   - "alias": "state_chart",
158   - "name": "State Chart",
  141 + "alias": "basic_timeseries",
  142 + "name": "Timeseries - Flot",
159 143 "descriptor": {
160 144 "type": "timeseries",
161 145 "sizeX": 8,
... ... @@ -163,11 +147,27 @@
163 147 "resources": [],
164 148 "templateHtml": "",
165 149 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
166   - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  150 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
167 151 "settingsSchema": "{}",
168 152 "dataKeySettingsSchema": "{}",
169   - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
  153 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
  154 + }
  155 + },
  156 + {
  157 + "alias": "timeseries_bars_flot",
  158 + "name": "Timeseries Bars - Flot",
  159 + "descriptor": {
  160 + "type": "timeseries",
  161 + "sizeX": 8,
  162 + "sizeY": 5,
  163 + "resources": [],
  164 + "templateHtml": "",
  165 + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
  166 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  167 + "settingsSchema": "{}",
  168 + "dataKeySettingsSchema": "{}",
  169 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
170 170 }
171 171 }
172 172 ]
173   -}
  173 +}
\ No newline at end of file
... ...
  1 +{
  2 + "widgetsBundle": {
  3 + "alias": "navigation_widgets",
  4 + "title": "Navigation widgets",
  5 + "image": null
  6 + },
  7 + "widgetTypes": [
  8 + {
  9 + "alias": "navigation_cards",
  10 + "name": "Navigation cards",
  11 + "descriptor": {
  12 + "type": "static",
  13 + "sizeX": 7,
  14 + "sizeY": 6,
  15 + "resources": [],
  16 + "templateHtml": "<tb-navigation-cards-widget [ctx]=\"ctx\"></tb-navigation-cards-widget>",
  17 + "templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}",
  18 + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n",
  19 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"filterType\": {\n \"title\": \"Filter type\",\n \"type\": \"string\",\n \"default\": \"all\"\n },\n \"filter\": {\n \"title\": \"Items\",\n \"type\": \"array\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"filterType\",\n \"type\": \"radios\",\n \"direction\": \"row\",\n \"titleMap\": [\n {\n \"value\": \"all\",\n \"name\": \"All items\"\n },\n {\n \"value\": \"include\",\n \"name\": \"Include items\"\n },\n {\n \"value\": \"exclude\",\n \"name\": \"Exclude items\"\n }\n ]\n },\n {\n \"key\": \"filter\",\n \"type\": \"rc-select\",\n \"condition\": \"model.filterType !== 'all'\",\n \"tags\": true,\n \"placeholder\": \"Enter urls to filter\",\n \"items\": [{\"value\": \"/devices\", \"label\": \"/devices\"}, {\"value\": \"/assets\", \"label\": \"/assets\"}, {\"value\": \"/deviceProfies\", \"label\": \"/deviceProfies\"}]\n }\n ]\n}\n",
  20 + "dataKeySettingsSchema": "{}\n",
  21 + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
  22 + }
  23 + },
  24 + {
  25 + "alias": "navigation_card",
  26 + "name": "Navigation card",
  27 + "descriptor": {
  28 + "type": "static",
  29 + "sizeX": 2.5,
  30 + "sizeY": 2,
  31 + "resources": [],
  32 + "templateHtml": "<tb-navigation-card-widget [ctx]=\"ctx\"></tb-navigation-card-widget>",
  33 + "templateCss": "",
  34 + "controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n",
  35 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"name\": {\n \"title\": \"Title\",\n \"type\": \"string\",\n \"default\": \"{i18n:device.devices}\"\n },\n \"icon\": {\n \"title\": \"icon\",\n \"type\": \"string\",\n \"default\": \"devices_other\"\n },\n \"path\": {\n \"title\": \"Navigation path\",\n \"type\": \"string\",\n \"default\": \"/devices\"\n }\n },\n \"required\": [\"name\", \"icon\", \"path\"]\n },\n \"form\": [\n \"name\",\n {\n \"key\": \"icon\",\n \"type\": \"icon\"\n },\n \"path\"\n ]\n}\n",
  36 + "dataKeySettingsSchema": "{}\n",
  37 + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
  38 + }
  39 + }
  40 + ]
  41 +}
\ No newline at end of file
... ...
... ... @@ -15,6 +15,8 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 20 import org.springframework.beans.factory.annotation.Value;
19 21 import org.springframework.http.HttpStatus;
20 22 import org.springframework.security.access.prepost.PreAuthorize;
... ... @@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer;
30 32 import org.thingsboard.server.common.data.Dashboard;
31 33 import org.thingsboard.server.common.data.DashboardInfo;
32 34 import org.thingsboard.server.common.data.EntityType;
  35 +import org.thingsboard.server.common.data.HomeDashboard;
  36 +import org.thingsboard.server.common.data.HomeDashboardInfo;
33 37 import org.thingsboard.server.common.data.ShortCustomerInfo;
  38 +import org.thingsboard.server.common.data.Tenant;
  39 +import org.thingsboard.server.common.data.User;
34 40 import org.thingsboard.server.common.data.audit.ActionType;
35 41 import org.thingsboard.server.common.data.exception.ThingsboardException;
36 42 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -39,7 +45,9 @@ import org.thingsboard.server.common.data.id.TenantId;
39 45 import org.thingsboard.server.common.data.page.PageData;
40 46 import org.thingsboard.server.common.data.page.PageLink;
41 47 import org.thingsboard.server.common.data.page.TimePageLink;
  48 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
42 49 import org.thingsboard.server.queue.util.TbCoreComponent;
  50 +import org.thingsboard.server.service.security.model.SecurityUser;
43 51 import org.thingsboard.server.service.security.permission.Operation;
44 52 import org.thingsboard.server.service.security.permission.Resource;
45 53
... ... @@ -53,6 +61,9 @@ public class DashboardController extends BaseController {
53 61
54 62 public static final String DASHBOARD_ID = "dashboardId";
55 63
  64 + private static final String HOME_DASHBOARD_ID = "homeDashboardId";
  65 + private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
  66 +
56 67 @Value("${dashboard.max_datapoints_limit}")
57 68 private long maxDatapointsLimit;
58 69
... ... @@ -472,4 +483,100 @@ public class DashboardController extends BaseController {
472 483 throw handleException(e);
473 484 }
474 485 }
  486 +
  487 + @PreAuthorize("isAuthenticated()")
  488 + @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET)
  489 + @ResponseBody
  490 + public HomeDashboard getHomeDashboard() throws ThingsboardException {
  491 + try {
  492 + SecurityUser securityUser = getCurrentUser();
  493 + if (securityUser.isSystemAdmin()) {
  494 + return null;
  495 + }
  496 + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
  497 + JsonNode additionalInfo = user.getAdditionalInfo();
  498 + HomeDashboard homeDashboard;
  499 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  500 + if (homeDashboard == null) {
  501 + if (securityUser.isCustomerUser()) {
  502 + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId());
  503 + additionalInfo = customer.getAdditionalInfo();
  504 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  505 + }
  506 + if (homeDashboard == null) {
  507 + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId());
  508 + additionalInfo = tenant.getAdditionalInfo();
  509 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  510 + }
  511 + }
  512 + return homeDashboard;
  513 + } catch (Exception e) {
  514 + throw handleException(e);
  515 + }
  516 + }
  517 +
  518 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  519 + @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET)
  520 + @ResponseBody
  521 + public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException {
  522 + try {
  523 + Tenant tenant = tenantService.findTenantById(getTenantId());
  524 + JsonNode additionalInfo = tenant.getAdditionalInfo();
  525 + DashboardId dashboardId = null;
  526 + boolean hideDashboardToolbar = true;
  527 + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
  528 + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
  529 + dashboardId = new DashboardId(toUUID(strDashboardId));
  530 + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
  531 + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
  532 + }
  533 + }
  534 + return new HomeDashboardInfo(dashboardId, hideDashboardToolbar);
  535 + } catch (Exception e) {
  536 + throw handleException(e);
  537 + }
  538 + }
  539 +
  540 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  541 + @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST)
  542 + @ResponseStatus(value = HttpStatus.OK)
  543 + public void setTenantHomeDashboardInfo(@RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException {
  544 + try {
  545 + if (homeDashboardInfo.getDashboardId() != null) {
  546 + checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ);
  547 + }
  548 + Tenant tenant = tenantService.findTenantById(getTenantId());
  549 + JsonNode additionalInfo = tenant.getAdditionalInfo();
  550 + if (additionalInfo == null || !(additionalInfo instanceof ObjectNode)) {
  551 + additionalInfo = JacksonUtil.OBJECT_MAPPER.createObjectNode();
  552 + }
  553 + if (homeDashboardInfo.getDashboardId() != null) {
  554 + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_ID, homeDashboardInfo.getDashboardId().getId().toString());
  555 + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_HIDE_TOOLBAR, homeDashboardInfo.isHideDashboardToolbar());
  556 + } else {
  557 + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_ID);
  558 + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_HIDE_TOOLBAR);
  559 + }
  560 + tenant.setAdditionalInfo(additionalInfo);
  561 + tenantService.saveTenant(tenant);
  562 + } catch (Exception e) {
  563 + throw handleException(e);
  564 + }
  565 + }
  566 +
  567 + private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) {
  568 + try {
  569 + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
  570 + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
  571 + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
  572 + Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
  573 + boolean hideDashboardToolbar = true;
  574 + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
  575 + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
  576 + }
  577 + return new HomeDashboard(dashboard, hideDashboardToolbar);
  578 + }
  579 + } catch (Exception e) {}
  580 + return null;
  581 + }
475 582 }
... ...
... ... @@ -185,6 +185,8 @@ public class ThingsboardInstallService {
185 185 case "3.2.0":
186 186 log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
187 187 databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
  188 + case "3.2.1":
  189 + log.info("Upgrading ThingsBoard from version 3.2.1 to 3.3.0 ...");
188 190 log.info("Updating system data...");
189 191 systemDataLoaderService.updateSystemWidgets();
190 192 break;
... ...
... ... @@ -197,7 +197,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
197 197 generalSettings.setKey("general");
198 198 ObjectNode node = objectMapper.createObjectNode();
199 199 node.put("baseUrl", "http://localhost:8080");
200   - node.put("prohibitDifferentUrl", true);
  200 + node.put("prohibitDifferentUrl", false);
201 201 generalSettings.setJsonValue(node);
202 202 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
203 203
... ... @@ -438,6 +438,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
438 438 this.deleteSystemWidgetBundle("input_widgets");
439 439 this.deleteSystemWidgetBundle("date");
440 440 this.deleteSystemWidgetBundle("entity_admin_widgets");
  441 + this.deleteSystemWidgetBundle("navigation_widgets");
441 442 installScripts.loadSystemWidgets();
442 443 }
443 444
... ...
... ... @@ -15,11 +15,20 @@
15 15 */
16 16 package org.thingsboard.server.service.install;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 19 import org.springframework.context.annotation.Profile;
19 20 import org.springframework.stereotype.Service;
20 21 import org.thingsboard.server.dao.util.HsqlDao;
21 22
  23 +import java.nio.charset.Charset;
  24 +import java.nio.file.Files;
  25 +import java.nio.file.Path;
  26 +import java.nio.file.Paths;
  27 +import java.sql.Connection;
  28 +import java.sql.DriverManager;
  29 +
22 30 @Service
  31 +@Slf4j
23 32 @HsqlDao
24 33 @Profile("install")
25 34 public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
... ... @@ -27,5 +36,21 @@ public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSe
27 36 protected HsqlEntityDatabaseSchemaService() {
28 37 super("schema-entities-hsql.sql", "schema-entities-idx.sql");
29 38 }
  39 +
  40 + private final String schemaTypesSql = "schema-types-hsql.sql";
  41 +
  42 + @Override
  43 + public void createDatabaseSchema(boolean createIndexes) throws Exception {
  44 +
  45 + log.info("Installing SQL DataBase types part: " + schemaTypesSql);
  46 +
  47 + Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaTypesSql);
  48 + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
  49 + String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8"));
  50 + conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
  51 + }
  52 +
  53 + super.createDatabaseSchema(createIndexes);
  54 + }
30 55 }
31 56
... ...
... ... @@ -30,7 +30,7 @@ import java.sql.SQLException;
30 30 @Slf4j
31 31 public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchemaService {
32 32
33   - private static final String SQL_DIR = "sql";
  33 + protected static final String SQL_DIR = "sql";
34 34
35 35 @Value("${spring.datasource.url}")
36 36 protected String dbUrl;
... ... @@ -42,7 +42,7 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
42 42 protected String dbPassword;
43 43
44 44 @Autowired
45   - private InstallScripts installScripts;
  45 + protected InstallScripts installScripts;
46 46
47 47 private final String schemaSql;
48 48 private final String schemaIdxSql;
... ...
... ... @@ -59,6 +59,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
59 59 import org.thingsboard.server.queue.util.TbCoreComponent;
60 60 import org.thingsboard.server.service.queue.TbClusterService;
61 61 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
  62 +import org.thingsboard.server.utils.EventDeduplicationExecutor;
62 63
63 64 import javax.annotation.Nullable;
64 65 import javax.annotation.PostConstruct;
... ... @@ -126,13 +127,13 @@ public class DefaultDeviceStateService implements DeviceStateService {
126 127 @Getter
127 128 private int initFetchPackSize;
128 129
129   - private volatile boolean clusterUpdatePending = false;
130   -
131 130 private ListeningScheduledExecutorService queueExecutor;
132 131 private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>();
133 132 private final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
134 133 private final ConcurrentMap<DeviceId, Long> deviceLastReportedActivity = new ConcurrentHashMap<>();
135 134 private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>();
  135 + private volatile EventDeduplicationExecutor<Set<TopicPartitionInfo>> deduplicationExecutor;
  136 +
136 137
137 138 public DefaultDeviceStateService(TenantService tenantService, DeviceService deviceService,
138 139 AttributesService attributesService, TimeseriesService tsService,
... ... @@ -155,6 +156,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
155 156 // Should be always single threaded due to absence of locks.
156 157 queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-state")));
157 158 queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
  159 + deduplicationExecutor = new EventDeduplicationExecutor<>(DefaultDeviceStateService.class.getSimpleName(), queueExecutor, this::initStateFromDB);
158 160 }
159 161
160 162 @PreDestroy
... ... @@ -292,25 +294,14 @@ public class DefaultDeviceStateService implements DeviceStateService {
292 294 }
293 295 }
294 296
295   - volatile Set<TopicPartitionInfo> pendingPartitions;
296   -
297 297 @Override
298 298 public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
299 299 if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
300   - synchronized (this) {
301   - pendingPartitions = partitionChangeEvent.getPartitions();
302   - if (!clusterUpdatePending) {
303   - clusterUpdatePending = true;
304   - queueExecutor.submit(() -> {
305   - clusterUpdatePending = false;
306   - initStateFromDB();
307   - });
308   - }
309   - }
  300 + deduplicationExecutor.submit(partitionChangeEvent.getPartitions());
310 301 }
311 302 }
312 303
313   - private void initStateFromDB() {
  304 + private void initStateFromDB(Set<TopicPartitionInfo> pendingPartitions) {
314 305 try {
315 306 log.info("CURRENT PARTITIONS: {}", partitionedDevices.keySet());
316 307 log.info("NEW PARTITIONS: {}", pendingPartitions);
... ...
... ... @@ -302,7 +302,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
302 302 Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
303 303 TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
304 304 attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
305   - ctx.setAndResolveQuery(cmd.getQuery());
  305 + if (cmd.getQuery() != null) {
  306 + ctx.setAndResolveQuery(cmd.getQuery());
  307 + }
306 308 sessionSubs.put(cmd.getCmdId(), ctx);
307 309 return ctx;
308 310 }
... ...
... ... @@ -107,7 +107,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
107 107 public void setAndResolveQuery(T query) {
108 108 dynamicValues.clear();
109 109 this.query = query;
110   - if (query.getKeyFilters() != null) {
  110 + if (query != null && query.getKeyFilters() != null) {
111 111 for (KeyFilter filter : query.getKeyFilters()) {
112 112 registerDynamicValues(filter.getPredicate());
113 113 }
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.utils;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +
  20 +import java.util.concurrent.Executor;
  21 +import java.util.concurrent.ExecutorService;
  22 +import java.util.function.Consumer;
  23 +
  24 +/**
  25 + * This class deduplicate executions of the specified function.
  26 + * Useful in cluster mode, when you get event about partition change multiple times.
  27 + * Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and
  28 + * later, once the processing of first event is done, process last pending task.
  29 + *
  30 + * @param <P> parameters of the function
  31 + */
  32 +@Slf4j
  33 +public class EventDeduplicationExecutor<P> {
  34 + private final String name;
  35 + private final ExecutorService executor;
  36 + private final Consumer<P> function;
  37 + private P pendingTask;
  38 + private boolean busy;
  39 +
  40 + public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer<P> function) {
  41 + this.name = name;
  42 + this.executor = executor;
  43 + this.function = function;
  44 + }
  45 +
  46 + public void submit(P params) {
  47 + log.info("[{}] Going to submit: {}", name, params);
  48 + synchronized (EventDeduplicationExecutor.this) {
  49 + if (!busy) {
  50 + busy = true;
  51 + pendingTask = null;
  52 + try {
  53 + log.info("[{}] Submitting task: {}", name, params);
  54 + executor.submit(() -> {
  55 + try {
  56 + log.info("[{}] Executing task: {}", name, params);
  57 + function.accept(params);
  58 + } catch (Throwable e) {
  59 + log.warn("[{}] Failed to process task with parameters: {}", name, params, e);
  60 + throw e;
  61 + } finally {
  62 + unlockAndProcessIfAny();
  63 + }
  64 + });
  65 + } catch (Throwable e) {
  66 + log.warn("[{}] Failed to submit task with parameters: {}", name, params, e);
  67 + unlockAndProcessIfAny();
  68 + throw e;
  69 + }
  70 + } else {
  71 + log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params);
  72 + pendingTask = params;
  73 + }
  74 + }
  75 + }
  76 +
  77 + private void unlockAndProcessIfAny() {
  78 + synchronized (EventDeduplicationExecutor.this) {
  79 + busy = false;
  80 + if (pendingTask != null) {
  81 + submit(pendingTask);
  82 + }
  83 + }
  84 + }
  85 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.util;
  17 +
  18 +import com.google.common.util.concurrent.MoreExecutors;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.junit.Test;
  21 +import org.junit.runner.RunWith;
  22 +import org.mockito.Mockito;
  23 +import org.mockito.runners.MockitoJUnitRunner;
  24 +import org.thingsboard.server.utils.EventDeduplicationExecutor;
  25 +
  26 +import java.util.concurrent.ExecutorService;
  27 +import java.util.concurrent.Executors;
  28 +import java.util.function.Consumer;
  29 +
  30 +@Slf4j
  31 +@RunWith(MockitoJUnitRunner.class)
  32 +public class EventDeduplicationExecutorTest {
  33 +
  34 + @Test
  35 + public void testSimpleFlowSameThread() throws InterruptedException {
  36 + simpleFlow(MoreExecutors.newDirectExecutorService());
  37 + }
  38 +
  39 + @Test
  40 + public void testPeriodicFlowSameThread() throws InterruptedException {
  41 + periodicFlow(MoreExecutors.newDirectExecutorService());
  42 + }
  43 +
  44 + @Test
  45 + public void testExceptionFlowSameThread() throws InterruptedException {
  46 + exceptionFlow(MoreExecutors.newDirectExecutorService());
  47 + }
  48 +
  49 + @Test
  50 + public void testSimpleFlowSingleThread() throws InterruptedException {
  51 + simpleFlow(Executors.newSingleThreadExecutor());
  52 + }
  53 +
  54 + @Test
  55 + public void testPeriodicFlowSingleThread() throws InterruptedException {
  56 + periodicFlow(Executors.newSingleThreadExecutor());
  57 + }
  58 +
  59 + @Test
  60 + public void testExceptionFlowSingleThread() throws InterruptedException {
  61 + exceptionFlow(Executors.newSingleThreadExecutor());
  62 + }
  63 +
  64 + @Test
  65 + public void testSimpleFlowMultiThread() throws InterruptedException {
  66 + simpleFlow(Executors.newFixedThreadPool(3));
  67 + }
  68 +
  69 + @Test
  70 + public void testPeriodicFlowMultiThread() throws InterruptedException {
  71 + periodicFlow(Executors.newFixedThreadPool(3));
  72 + }
  73 +
  74 + @Test
  75 + public void testExceptionFlowMultiThread() throws InterruptedException {
  76 + exceptionFlow(Executors.newFixedThreadPool(3));
  77 + }
  78 +
  79 + private void simpleFlow(ExecutorService executorService) throws InterruptedException {
  80 + try {
  81 + Consumer<String> function = Mockito.spy(StringConsumer.class);
  82 + EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
  83 +
  84 + String params1 = "params1";
  85 + String params2 = "params2";
  86 + String params3 = "params3";
  87 +
  88 + executor.submit(params1);
  89 + executor.submit(params2);
  90 + executor.submit(params3);
  91 + Thread.sleep(500);
  92 + Mockito.verify(function).accept(params1);
  93 + Mockito.verify(function).accept(params3);
  94 + } finally {
  95 + executorService.shutdownNow();
  96 + }
  97 + }
  98 +
  99 + private void periodicFlow(ExecutorService executorService) throws InterruptedException {
  100 + try {
  101 + Consumer<String> function = Mockito.spy(StringConsumer.class);
  102 + EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
  103 +
  104 + String params1 = "params1";
  105 + String params2 = "params2";
  106 + String params3 = "params3";
  107 +
  108 + executor.submit(params1);
  109 + Thread.sleep(500);
  110 + executor.submit(params2);
  111 + Thread.sleep(500);
  112 + executor.submit(params3);
  113 + Thread.sleep(500);
  114 + Mockito.verify(function).accept(params1);
  115 + Mockito.verify(function).accept(params2);
  116 + Mockito.verify(function).accept(params3);
  117 + } finally {
  118 + executorService.shutdownNow();
  119 + }
  120 + }
  121 +
  122 + private void exceptionFlow(ExecutorService executorService) throws InterruptedException {
  123 + try {
  124 + Consumer<String> function = Mockito.spy(StringConsumer.class);
  125 + EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
  126 +
  127 + String params1 = "params1";
  128 + String params2 = "params2";
  129 + String params3 = "params3";
  130 +
  131 + Mockito.doThrow(new RuntimeException()).when(function).accept("params1");
  132 + executor.submit(params1);
  133 + executor.submit(params2);
  134 + Thread.sleep(500);
  135 + executor.submit(params3);
  136 + Thread.sleep(500);
  137 + Mockito.verify(function).accept(params2);
  138 + Mockito.verify(function).accept(params3);
  139 + } finally {
  140 + executorService.shutdownNow();
  141 + }
  142 + }
  143 +
  144 + public static class StringConsumer implements Consumer<String> {
  145 + @Override
  146 + public void accept(String s) {
  147 + try {
  148 + Thread.sleep(100);
  149 + } catch (InterruptedException e) {
  150 + throw new RuntimeException(e);
  151 + }
  152 + }
  153 + }
  154 +
  155 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class HomeDashboard extends Dashboard {
  22 +
  23 + private boolean hideDashboardToolbar;
  24 +
  25 + public HomeDashboard(Dashboard dashboard, boolean hideDashboardToolbar) {
  26 + super(dashboard);
  27 + this.hideDashboardToolbar = hideDashboardToolbar;
  28 + }
  29 +
  30 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data;
  17 +
  18 +import lombok.AllArgsConstructor;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.data.id.DashboardId;
  21 +
  22 +@Data
  23 +@AllArgsConstructor
  24 +public class HomeDashboardInfo {
  25 + private DashboardId dashboardId;
  26 + private boolean hideDashboardToolbar;
  27 +}
... ...
... ... @@ -19,16 +19,18 @@ import com.google.common.util.concurrent.ListenableFuture;
19 19 import org.thingsboard.server.common.data.alarm.Alarm;
20 20 import org.thingsboard.server.common.data.alarm.AlarmInfo;
21 21 import org.thingsboard.server.common.data.alarm.AlarmQuery;
  22 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
  23 +import org.thingsboard.server.common.data.alarm.AlarmStatus;
22 24 import org.thingsboard.server.common.data.id.CustomerId;
23 25 import org.thingsboard.server.common.data.id.EntityId;
24 26 import org.thingsboard.server.common.data.id.TenantId;
25 27 import org.thingsboard.server.common.data.page.PageData;
26 28 import org.thingsboard.server.common.data.query.AlarmData;
27   -import org.thingsboard.server.common.data.query.AlarmDataPageLink;
28 29 import org.thingsboard.server.common.data.query.AlarmDataQuery;
29 30 import org.thingsboard.server.dao.Dao;
30 31
31 32 import java.util.Collection;
  33 +import java.util.Set;
32 34 import java.util.UUID;
33 35
34 36 /**
... ... @@ -48,4 +50,6 @@ public interface AlarmDao extends Dao<Alarm> {
48 50
49 51 PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId,
50 52 AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
  53 +
  54 + Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status);
51 55 }
... ...
... ... @@ -39,10 +39,10 @@ import org.thingsboard.server.common.data.id.CustomerId;
39 39 import org.thingsboard.server.common.data.id.EntityId;
40 40 import org.thingsboard.server.common.data.id.TenantId;
41 41 import org.thingsboard.server.common.data.page.PageData;
42   -import org.thingsboard.server.common.data.page.TimePageLink;
43 42 import org.thingsboard.server.common.data.query.AlarmData;
44 43 import org.thingsboard.server.common.data.query.AlarmDataPageLink;
45 44 import org.thingsboard.server.common.data.query.AlarmDataQuery;
  45 +import org.thingsboard.server.common.data.query.DeviceTypeFilter;
46 46 import org.thingsboard.server.common.data.relation.EntityRelation;
47 47 import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
48 48 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
... ... @@ -60,7 +60,6 @@ import javax.annotation.PreDestroy;
60 60 import java.util.ArrayList;
61 61 import java.util.Collection;
62 62 import java.util.Collections;
63   -import java.util.Comparator;
64 63 import java.util.LinkedHashSet;
65 64 import java.util.List;
66 65 import java.util.Set;
... ... @@ -316,37 +315,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
316 315 @Override
317 316 public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
318 317 AlarmStatus alarmStatus) {
319   - TimePageLink nextPageLink = new TimePageLink(100);
320   - boolean hasNext = true;
321   - AlarmSeverity highestSeverity = null;
322   - AlarmQuery query;
323   - while (hasNext && AlarmSeverity.CRITICAL != highestSeverity) {
324   - query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false, null);
325   - PageData<AlarmInfo> alarms = alarmDao.findAlarms(tenantId, query);
326   - if (alarms.hasNext()) {
327   - nextPageLink = nextPageLink.nextPageLink();
328   - }
329   - AlarmSeverity severity = detectHighestSeverity(alarms.getData());
330   - if (severity == null) {
331   - continue;
332   - }
333   - if (severity == AlarmSeverity.CRITICAL || highestSeverity == null) {
334   - highestSeverity = severity;
335   - } else {
336   - highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity;
337   - }
  318 + Set<AlarmStatus> statusList = null;
  319 + if (alarmSearchStatus != null) {
  320 + statusList = alarmSearchStatus.getStatuses();
  321 + } else if (alarmStatus != null) {
  322 + statusList = Collections.singleton(alarmStatus);
338 323 }
339   - return highestSeverity;
340   - }
341 324
342   - private AlarmSeverity detectHighestSeverity(List<AlarmInfo> alarms) {
343   - if (!alarms.isEmpty()) {
344   - List<AlarmInfo> sorted = new ArrayList(alarms);
345   - sorted.sort(Comparator.comparing(Alarm::getSeverity));
346   - return sorted.get(0).getSeverity();
347   - } else {
348   - return null;
349   - }
  325 + Set<AlarmSeverity> alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList);
  326 +
  327 + return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null);
350 328 }
351 329
352 330 private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) {
... ...
... ... @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable;
20 20 import org.springframework.data.jpa.repository.Query;
21 21 import org.springframework.data.repository.CrudRepository;
22 22 import org.springframework.data.repository.query.Param;
  23 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
23 24 import org.thingsboard.server.common.data.alarm.AlarmStatus;
24 25 import org.thingsboard.server.dao.model.sql.AlarmEntity;
25 26 import org.thingsboard.server.dao.model.sql.AlarmInfoEntity;
... ... @@ -75,4 +76,12 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
75 76 @Param("searchText") String searchText,
76 77 Pageable pageable);
77 78
  79 + @Query("SELECT alarm.severity FROM AlarmEntity alarm" +
  80 + " WHERE alarm.tenantId = :tenantId" +
  81 + " AND alarm.originatorId = :entityId" +
  82 + " AND ((:status) IS NULL OR alarm.status in (:status))")
  83 + Set<AlarmSeverity> findAlarmSeverities(@Param("tenantId") UUID tenantId,
  84 + @Param("entityId") UUID entityId,
  85 + @Param("status") Set<AlarmStatus> status);
  86 +
78 87 }
... ...
... ... @@ -24,6 +24,7 @@ import org.springframework.stereotype.Component;
24 24 import org.thingsboard.server.common.data.alarm.Alarm;
25 25 import org.thingsboard.server.common.data.alarm.AlarmInfo;
26 26 import org.thingsboard.server.common.data.alarm.AlarmQuery;
  27 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
27 28 import org.thingsboard.server.common.data.alarm.AlarmStatus;
28 29 import org.thingsboard.server.common.data.id.CustomerId;
29 30 import org.thingsboard.server.common.data.id.EntityId;
... ... @@ -120,4 +121,9 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
120 121 public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
121 122 return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds);
122 123 }
  124 +
  125 + @Override
  126 + public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status) {
  127 + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status);
  128 + }
123 129 }
... ...
... ... @@ -83,7 +83,7 @@ public class EntityDataAdapter {
83 83 if (value != null) {
84 84 String strVal = value.toString();
85 85 // check number
86   - if (strVal.length() > 0 && NumberUtils.isParsable(strVal)) {
  86 + if (NumberUtils.isNumber(strVal)) {
87 87 try {
88 88 long longVal = Long.parseLong(strVal);
89 89 return Long.toString(longVal);
... ...
... ... @@ -116,16 +116,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
116 116 super.startExecutor();
117 117 if (!isInstall()) {
118 118 getFetchStmt(Aggregation.NONE, DESC_ORDER);
119   - Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);
120   - if (partition.isPresent()) {
121   - tsFormat = partition.get();
122   - if (!isFixedPartitioning() && partitionsCacheSize > 0) {
123   - cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize);
124   - }
125   - } else {
126   - log.warn("Incorrect configuration of partitioning {}", partitioning);
127   - throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
  119 + }
  120 + Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);
  121 + if (partition.isPresent()) {
  122 + tsFormat = partition.get();
  123 + if (!isFixedPartitioning() && partitionsCacheSize > 0) {
  124 + cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize);
128 125 }
  126 + } else {
  127 + log.warn("Incorrect configuration of partitioning {}", partitioning);
  128 + throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
129 129 }
130 130 }
131 131
... ...
... ... @@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS customer (
114 114 CREATE TABLE IF NOT EXISTS dashboard (
115 115 id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
116 116 created_time bigint NOT NULL,
117   - configuration varchar,
  117 + configuration varchar(10000000),
118 118 assigned_customers varchar(1000000),
119 119 search_text varchar(255),
120 120 tenant_id uuid,
... ...
... ... @@ -355,6 +355,62 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
355 355 }
356 356
357 357 @Test
  358 + public void testFindHighestAlarmSeverity() throws ExecutionException, InterruptedException {
  359 + Customer customer = new Customer();
  360 + customer.setTitle("TestCustomer");
  361 + customer.setTenantId(tenantId);
  362 + customer = customerService.saveCustomer(customer);
  363 +
  364 + Device customerDevice = new Device();
  365 + customerDevice.setName("TestCustomerDevice");
  366 + customerDevice.setType("default");
  367 + customerDevice.setTenantId(tenantId);
  368 + customerDevice.setCustomerId(customer.getId());
  369 + customerDevice = deviceService.saveDevice(customerDevice);
  370 +
  371 + // no one alarms was created
  372 + Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null));
  373 +
  374 + Alarm alarm1 = Alarm.builder()
  375 + .tenantId(tenantId)
  376 + .originator(customerDevice.getId())
  377 + .type(TEST_ALARM)
  378 + .severity(AlarmSeverity.MAJOR)
  379 + .status(AlarmStatus.ACTIVE_UNACK)
  380 + .startTs(System.currentTimeMillis())
  381 + .build();
  382 + alarm1 = alarmService.createOrUpdateAlarm(alarm1).getAlarm();
  383 + alarmService.clearAlarm(tenantId, alarm1.getId(), null, System.currentTimeMillis()).get();
  384 +
  385 + Alarm alarm2 = Alarm.builder()
  386 + .tenantId(tenantId)
  387 + .originator(customerDevice.getId())
  388 + .type(TEST_ALARM)
  389 + .severity(AlarmSeverity.MINOR)
  390 + .status(AlarmStatus.ACTIVE_ACK)
  391 + .startTs(System.currentTimeMillis())
  392 + .build();
  393 + alarm2 = alarmService.createOrUpdateAlarm(alarm2).getAlarm();
  394 + alarmService.clearAlarm(tenantId, alarm2.getId(), null, System.currentTimeMillis()).get();
  395 +
  396 + Alarm alarm3 = Alarm.builder()
  397 + .tenantId(tenantId)
  398 + .originator(customerDevice.getId())
  399 + .type(TEST_ALARM)
  400 + .severity(AlarmSeverity.CRITICAL)
  401 + .status(AlarmStatus.ACTIVE_ACK)
  402 + .startTs(System.currentTimeMillis())
  403 + .build();
  404 + alarm3 = alarmService.createOrUpdateAlarm(alarm3).getAlarm();
  405 +
  406 + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null));
  407 + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null));
  408 + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK));
  409 + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null));
  410 + Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK));
  411 + }
  412 +
  413 + @Test
358 414 public void testFindAlarmUsingAlarmDataQuery() throws ExecutionException, InterruptedException {
359 415 AssetId parentId = new AssetId(Uuids.timeBased());
360 416 AssetId parentId2 = new AssetId(Uuids.timeBased());
... ...
... ... @@ -827,10 +827,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
827 827 .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
828 828 }
829 829 List<String> deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
830   - if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) {
831   - // in H2 double values are stored with E0 in the end of the string
832   - loadedTemperatures = loadedTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList());
833   - }
  830 +
834 831 Assert.assertEquals(deviceTemperatures, loadedTemperatures);
835 832
836 833 pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
... ... @@ -858,10 +855,6 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
858 855 entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList());
859 856 List<String> deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
860 857
861   - if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) {
862   - // in H2 double values are stored with E0 in the end of the string
863   - loadedHighTemperatures = loadedHighTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList());
864   - }
865 858 Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
866 859
867 860 deviceService.deleteDevicesByTenantId(tenantId);
... ...
... ... @@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
18 18
19 19 RUN apt-get update
20 20 RUN apt-get install -y curl nmap procps
21   -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
  21 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
22 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
23 23 RUN echo 'deb http://www.apache.org/dist/cassandra/debian 311x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
24 24 RUN curl -L https://www.apache.org/dist/cassandra/KEYS | apt-key add -
... ...
... ... @@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
18 18
19 19 RUN apt-get update
20 20 RUN apt-get install -y curl
21   -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
  21 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
22 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
23 23 ENV PG_MAJOR 11
24 24 RUN apt-get update
... ...
... ... @@ -1189,7 +1189,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1189 1189
1190 1190 public List<EntitySubtype> getDeviceTypes() {
1191 1191 return restTemplate.exchange(
1192   - baseURL + "/api/devices",
  1192 + baseURL + "/api/device/types",
1193 1193 HttpMethod.GET,
1194 1194 HttpEntity.EMPTY,
1195 1195 new ParameterizedTypeReference<List<EntitySubtype>>() {
... ...
... ... @@ -61,4 +61,12 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
61 61 configuration.setCredentials(new AnonymousCredentials());
62 62 return configuration;
63 63 }
  64 +
  65 + public ClientCredentials getCredentials() {
  66 + if (this.credentials == null) {
  67 + return new AnonymousCredentials();
  68 + } else {
  69 + return this.credentials;
  70 + }
  71 + }
64 72 }
... ...
... ... @@ -53,8 +53,8 @@ export class AliasController implements IAliasController {
53 53 private stateControllerHolder: StateControllerHolder,
54 54 private origEntityAliases: EntityAliases,
55 55 private origFilters: Filters) {
56   - this.entityAliases = deepClone(this.origEntityAliases);
57   - this.filters = deepClone(this.origFilters);
  56 + this.entityAliases = deepClone(this.origEntityAliases) || {};
  57 + this.filters = deepClone(this.origFilters) || {};
58 58 this.userFilters = {};
59 59 }
60 60
... ...
... ... @@ -15,7 +15,7 @@
15 15 ///
16 16
17 17 import { Injectable, NgZone } from '@angular/core';
18   -import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router';
  18 +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
19 19 import { AuthService } from '../auth/auth.service';
20 20 import { select, Store } from '@ngrx/store';
21 21 import { AppState } from '../core.state';
... ... @@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum';
28 28 import { DialogService } from '@core/services/dialog.service';
29 29 import { TranslateService } from '@ngx-translate/core';
30 30 import { UtilsService } from '@core/services/utils.service';
  31 +import { isObject } from '@core/utils';
31 32
32 33 @Injectable({
33 34 providedIn: 'root'
... ... @@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
35 36 export class AuthGuard implements CanActivate, CanActivateChild {
36 37
37 38 constructor(private store: Store<AppState>,
  39 + private router: Router,
38 40 private authService: AuthService,
39 41 private dialogService: DialogService,
40 42 private utils: UtilsService,
... ... @@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild {
115 117 if (data.auth && data.auth.indexOf(authority) === -1) {
116 118 this.dialogService.forbidden();
117 119 return of(false);
  120 + } else if (data.redirectTo) {
  121 + let redirect;
  122 + if (isObject(data.redirectTo)) {
  123 + redirect = data.redirectTo[authority];
  124 + } else {
  125 + redirect = data.redirectTo;
  126 + }
  127 + return of(this.router.parseUrl(redirect));
118 128 } else {
119 129 return of(true);
120 130 }
... ...
... ... @@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
20 20 import { HttpClient } from '@angular/common/http';
21 21 import { PageLink } from '@shared/models/page/page-link';
22 22 import { PageData } from '@shared/models/page/page-data';
23   -import { Dashboard, DashboardInfo } from '@shared/models/dashboard.models';
  23 +import { Dashboard, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models';
24 24 import { WINDOW } from '@core/services/window.service';
25 25 import { NavigationEnd, Router } from '@angular/router';
26 26 import { filter, map, publishReplay, refCount } from 'rxjs/operators';
... ... @@ -122,6 +122,19 @@ export class DashboardService {
122 122 defaultHttpOptionsFromConfig(config));
123 123 }
124 124
  125 + public getHomeDashboard(config?: RequestConfig): Observable<HomeDashboard> {
  126 + return this.http.get<HomeDashboard>('/api/dashboard/home', defaultHttpOptionsFromConfig(config));
  127 + }
  128 +
  129 + public getTenantHomeDashboardInfo(config?: RequestConfig): Observable<HomeDashboardInfo> {
  130 + return this.http.get<HomeDashboardInfo>('/api/tenant/dashboard/home/info', defaultHttpOptionsFromConfig(config));
  131 + }
  132 +
  133 + public setTenantHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo, config?: RequestConfig): Observable<any> {
  134 + return this.http.post<any>('/api/tenant/dashboard/home/info', homeDashboardInfo,
  135 + defaultHttpOptionsFromConfig(config));
  136 + }
  137 +
125 138 public getPublicDashboardLink(dashboard: DashboardInfo): string | null {
126 139 if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) {
127 140 const publicCustomers = dashboard.assignedCustomers
... ...
... ... @@ -223,6 +223,7 @@ export class MenuService {
223 223 name: 'home.home',
224 224 type: 'link',
225 225 path: '/home',
  226 + notExact: true,
226 227 icon: 'home'
227 228 },
228 229 {
... ... @@ -284,6 +285,13 @@ export class MenuService {
284 285 },
285 286 {
286 287 id: guid(),
  288 + name: 'admin.home-settings',
  289 + type: 'link',
  290 + path: '/settings/home',
  291 + icon: 'settings_applications'
  292 + },
  293 + {
  294 + id: guid(),
287 295 name: 'audit-log.audit-logs',
288 296 type: 'link',
289 297 path: '/auditLogs',
... ... @@ -402,6 +410,7 @@ export class MenuService {
402 410 name: 'home.home',
403 411 type: 'link',
404 412 path: '/home',
  413 + notExact: true,
405 414 icon: 'home'
406 415 },
407 416 {
... ...
... ... @@ -14,14 +14,23 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  17 +import {
  18 + Component,
  19 + Injector,
  20 + Input,
  21 + OnDestroy,
  22 + OnInit,
  23 + StaticProvider,
  24 + ViewChild,
  25 + ViewContainerRef
  26 +} from '@angular/core';
18 27 import { TooltipPosition } from '@angular/material/tooltip';
19 28 import { AliasInfo, IAliasController } from '@core/api/widget-api.models';
20 29 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
21 30 import { TranslateService } from '@ngx-translate/core';
22 31 import { Subscription } from 'rxjs';
23 32 import { BreakpointObserver } from '@angular/cdk/layout';
24   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  33 +import { ComponentPortal } from '@angular/cdk/portal';
25 34 import {
26 35 ALIASES_ENTITY_SELECT_PANEL_DATA,
27 36 AliasesEntitySelectPanelComponent,
... ... @@ -136,12 +145,12 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy {
136 145 overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector));
137 146 }
138 147
139   - private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): PortalInjector {
140   - const injectionTokens = new WeakMap<any, any>([
141   - [ALIASES_ENTITY_SELECT_PANEL_DATA, data],
142   - [OverlayRef, overlayRef]
143   - ]);
144   - return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  148 + private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): Injector {
  149 + const providers: StaticProvider[] = [
  150 + {provide: ALIASES_ENTITY_SELECT_PANEL_DATA, useValue: data},
  151 + {provide: OverlayRef, useValue: overlayRef}
  152 + ];
  153 + return Injector.create({parent: this.viewContainerRef.injector, providers});
145 154 }
146 155
147 156 private updateDisplayValue() {
... ...
... ... @@ -195,7 +195,8 @@
195 195 [length]="dataSource.total() | async"
196 196 [pageIndex]="pageLink.page"
197 197 [pageSize]="pageLink.pageSize"
198   - [pageSizeOptions]="[10, 20, 30]"></mat-paginator>
  198 + [pageSizeOptions]="[10, 20, 30]"
  199 + showFirstLastButtons></mat-paginator>
199 200 <ngx-hm-carousel fxFlex *ngIf="mode === 'widget' && widgetsList.length > 0"
200 201 #carousel
201 202 [(ngModel)]="widgetsCarouselIndex"
... ...
... ... @@ -19,9 +19,11 @@ import {
19 19 ChangeDetectionStrategy,
20 20 Component,
21 21 ElementRef,
  22 + Injector,
22 23 Input,
23 24 NgZone,
24 25 OnInit,
  26 + StaticProvider,
25 27 ViewChild,
26 28 ViewContainerRef
27 29 } from '@angular/core';
... ... @@ -62,7 +64,7 @@ import {
62 64 EditAttributeValuePanelComponent,
63 65 EditAttributeValuePanelData
64 66 } from './edit-attribute-value-panel.component';
65   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  67 +import { ComponentPortal } from '@angular/cdk/portal';
66 68 import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
67 69 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
68 70 import { DataKey, Datasource, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
... ... @@ -319,13 +321,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
319 321 overlayRef.backdropClick().subscribe(() => {
320 322 overlayRef.dispose();
321 323 });
322   - const injectionTokens = new WeakMap<any, any>([
323   - [EDIT_ATTRIBUTE_VALUE_PANEL_DATA, {
324   - attributeValue: attribute.value
325   - } as EditAttributeValuePanelData],
326   - [OverlayRef, overlayRef]
327   - ]);
328   - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  324 + const providers: StaticProvider[] = [
  325 + {
  326 + provide: EDIT_ATTRIBUTE_VALUE_PANEL_DATA,
  327 + useValue: {
  328 + attributeValue: attribute.value
  329 + } as EditAttributeValuePanelData
  330 + },
  331 + {
  332 + provide: OverlayRef,
  333 + useValue: overlayRef
  334 + }
  335 + ];
  336 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
329 337 const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent,
330 338 this.viewContainerRef, injector));
331 339 componentRef.onDestroy(() => {
... ...
... ... @@ -49,10 +49,11 @@
49 49 <tb-states-component fxFlex.lt-md
50 50 [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
51 51 [dashboardCtrl]="this"
52   - [dashboardId]="dashboard.id ? dashboard.id.id : ''"
  52 + [dashboardId]="(!embedded && dashboard.id) ? dashboard.id.id : ''"
53 53 [isMobile]="isMobile"
54 54 [state]="dashboardCtx.state"
55 55 [currentState]="currentState"
  56 + [syncStateWithQueryParam]="syncStateWithQueryParam"
56 57 [states]="dashboardConfiguration.states">
57 58 </tb-states-component>
58 59 </div>
... ... @@ -78,7 +79,7 @@
78 79 (click)="isFullscreen = !isFullscreen">
79 80 <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
80 81 </button>
81   - <button [fxShow]="isEdit || displayExport()" mat-icon-button
  82 + <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button
82 83 matTooltip="{{'dashboard.export' | translate}}"
83 84 matTooltipPosition="below"
84 85 (click)="exportDashboard($event)">
... ... @@ -118,7 +119,7 @@
118 119 (click)="openDashboardSettings($event)">
119 120 <mat-icon>settings</mat-icon>
120 121 </button>
121   - <tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && displayDashboardsSelect()"
  122 + <tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()"
122 123 [(ngModel)]="currentDashboardId"
123 124 (ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
124 125 [customerId]="currentCustomerId"
... ...
... ... @@ -124,6 +124,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
124 124 hideToolbar: boolean;
125 125
126 126 @Input()
  127 + syncStateWithQueryParam = true;
  128 +
  129 + @Input()
127 130 dashboard: Dashboard;
128 131 dashboardConfiguration: DashboardConfiguration;
129 132
... ... @@ -266,9 +269,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
266 269 this.rxSubscriptions.push(this.route.data.subscribe(
267 270 (data) => {
268 271 if (this.embedded) {
269   - data.dashboard = this.dashboard;
  272 + data.dashboard = this.dashboardUtils.validateAndUpdateDashboard(this.dashboard);
  273 + data.currentDashboardId = this.dashboard.id ? this.dashboard.id.id : null;
270 274 data.widgetEditMode = false;
271 275 data.singlePageMode = false;
  276 + } else {
  277 + data.currentDashboardId = this.route.snapshot.params.dashboardId;
272 278 }
273 279 this.init(data);
274 280 this.runChangeDetection();
... ... @@ -286,7 +292,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
286 292
287 293 this.reset();
288 294
289   - this.currentDashboardId = this.route.snapshot.params.dashboardId;
  295 + this.currentDashboardId = data.currentDashboardId;
290 296
291 297 if (this.route.snapshot.params.customerId) {
292 298 this.currentCustomerId = this.route.snapshot.params.customerId;
... ...
... ... @@ -23,6 +23,7 @@
23 23 [widgetLayouts]="{}"
24 24 [isEdit]="false"
25 25 [isMobile]="true"
  26 + [disableWidgetInteraction]="true"
26 27 [isEditActionEnabled]="false"
27 28 [isExportActionEnabled]="false"
28 29 [isRemoveActionEnabled]="false"
... ... @@ -34,6 +35,7 @@
34 35 [widgetLayouts]="{}"
35 36 [isEdit]="false"
36 37 [isMobile]="true"
  38 + [disableWidgetInteraction]="true"
37 39 [isEditActionEnabled]="false"
38 40 [isExportActionEnabled]="false"
39 41 [isRemoveActionEnabled]="false"
... ... @@ -45,6 +47,7 @@
45 47 [widgetLayouts]="{}"
46 48 [isEdit]="false"
47 49 [isMobile]="true"
  50 + [disableWidgetInteraction]="true"
48 51 [isEditActionEnabled]="false"
49 52 [isExportActionEnabled]="false"
50 53 [isRemoveActionEnabled]="false"
... ... @@ -56,6 +59,7 @@
56 59 [widgetLayouts]="{}"
57 60 [isEdit]="false"
58 61 [isMobile]="true"
  62 + [disableWidgetInteraction]="true"
59 63 [isEditActionEnabled]="false"
60 64 [isExportActionEnabled]="false"
61 65 [isRemoveActionEnabled]="false"
... ... @@ -67,6 +71,7 @@
67 71 [widgetLayouts]="{}"
68 72 [isEdit]="false"
69 73 [isMobile]="true"
  74 + [disableWidgetInteraction]="true"
70 75 [isEditActionEnabled]="false"
71 76 [isExportActionEnabled]="false"
72 77 [isRemoveActionEnabled]="false"
... ...
... ... @@ -53,6 +53,7 @@
53 53 [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
54 54 [isMobile]="isMobile"
55 55 [isMobileDisabled]="widgetEditMode"
  56 + [disableWidgetInteraction]="isEdit"
56 57 [isEditActionEnabled]="isEdit"
57 58 [isExportActionEnabled]="isEdit && !widgetEditMode"
58 59 [isRemoveActionEnabled]="isEdit && !widgetEditMode"
... ...
... ... @@ -88,6 +88,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon
88 88
89 89 currentState: string;
90 90
  91 + syncStateWithQueryParam: boolean;
  92 +
91 93 private rxSubscriptions = new Array<Subscription>();
92 94
93 95 private inited = false;
... ... @@ -99,18 +101,20 @@ export abstract class StateControllerComponent implements IStateControllerCompon
99 101 }
100 102
101 103 ngOnInit(): void {
102   - this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
103   - const dashboardId = this.route.snapshot.params.dashboardId || '';
104   - if (this.dashboardId === dashboardId) {
105   - const newState = this.decodeStateParam(paramMap.get('state'));
106   - if (this.currentState !== newState) {
107   - this.currentState = newState;
108   - if (this.inited) {
109   - this.onStateChanged();
  104 + if (this.syncStateWithQueryParam) {
  105 + this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
  106 + const dashboardId = this.route.snapshot.params.dashboardId || '';
  107 + if (this.dashboardId === dashboardId) {
  108 + const newState = this.decodeStateParam(paramMap.get('state'));
  109 + if (this.currentState !== newState) {
  110 + this.currentState = newState;
  111 + if (this.inited) {
  112 + this.onStateChanged();
  113 + }
110 114 }
111 115 }
112   - }
113   - }));
  116 + }));
  117 + }
114 118 this.init();
115 119 this.inited = true;
116 120 }
... ... @@ -124,16 +128,18 @@ export abstract class StateControllerComponent implements IStateControllerCompon
124 128
125 129 protected updateStateParam(newState: string) {
126 130 this.currentState = newState;
127   - const queryParams: Params = { state: this.currentState };
128   - this.ngZone.run(() => {
129   - this.router.navigate(
130   - [],
131   - {
132   - relativeTo: this.route,
133   - queryParams,
134   - queryParamsHandling: 'merge',
135   - });
136   - });
  131 + if (this.syncStateWithQueryParam) {
  132 + const queryParams: Params = {state: this.currentState};
  133 + this.ngZone.run(() => {
  134 + this.router.navigate(
  135 + [],
  136 + {
  137 + relativeTo: this.route,
  138 + queryParams,
  139 + queryParamsHandling: 'merge',
  140 + });
  141 + });
  142 + }
137 143 }
138 144
139 145 public openRightLayout(): void {
... ...
... ... @@ -24,6 +24,7 @@ export interface IStateControllerComponent extends IStateController {
24 24 stateControllerInstanceId: string;
25 25 state: string;
26 26 currentState: string;
  27 + syncStateWithQueryParam: boolean;
27 28 isMobile: boolean;
28 29 states: {[id: string]: DashboardState };
29 30 dashboardId: string;
... ...
... ... @@ -54,6 +54,9 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
54 54 currentState: string;
55 55
56 56 @Input()
  57 + syncStateWithQueryParam: boolean;
  58 +
  59 + @Input()
57 60 isMobile: boolean;
58 61
59 62 stateControllerComponentRef: ComponentRef<IStateControllerComponent>;
... ... @@ -89,6 +92,8 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
89 92 this.stateControllerComponent.state = this.state;
90 93 } else if (propName === 'currentState') {
91 94 this.stateControllerComponent.currentState = this.currentState;
  95 + } else if (propName === 'syncStateWithQueryParam') {
  96 + this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam;
92 97 }
93 98 }
94 99 }
... ... @@ -119,6 +124,7 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
119 124 this.stateControllerComponent.stateControllerInstanceId = stateControllerInstanceId;
120 125 this.stateControllerComponent.state = this.state;
121 126 this.stateControllerComponent.currentState = this.currentState;
  127 + this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam;
122 128 this.stateControllerComponent.isMobile = this.isMobile;
123 129 this.stateControllerComponent.states = this.states;
124 130 this.stateControllerComponent.dashboardId = this.dashboardId;
... ...
... ... @@ -150,7 +150,7 @@
150 150 </button>
151 151 </div>
152 152 </div>
153   - <div fxFlex fxLayout="column" class="tb-widget-content">
  153 + <div fxFlex fxLayout="column" class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">
154 154 <tb-widget fxFlex
155 155 #widgetComponent
156 156 [dashboardWidget]="widget"
... ...
... ... @@ -128,6 +128,9 @@ div.tb-widget {
128 128 }
129 129
130 130 .tb-widget-content {
  131 + &.tb-no-interaction {
  132 + pointer-events: none;
  133 + }
131 134 tb-widget {
132 135 position: relative;
133 136 width: 100%;
... ...
... ... @@ -114,6 +114,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
114 114 isRemoveActionEnabled: boolean;
115 115
116 116 @Input()
  117 + disableWidgetInteraction = false;
  118 +
  119 + @Input()
117 120 dashboardStyle: {[klass: string]: any};
118 121
119 122 @Input()
... ...
... ... @@ -238,7 +238,8 @@
238 238 [length]="dataSource.total() | async"
239 239 [pageIndex]="pageLink.page"
240 240 [pageSize]="pageLink.pageSize"
241   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  241 + [pageSizeOptions]="pageSizeOptions"
  242 + showFirstLastButtons></mat-paginator>
242 243 </div>
243 244 </div>
244 245 </mat-drawer-content>
... ...
... ... @@ -44,7 +44,7 @@ import { EntityAction } from '@home/models/entity/entity-component.models';
44 44 import { Subscription } from 'rxjs';
45 45 import { MatTab, MatTabGroup } from '@angular/material/tabs';
46 46 import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
47   -import { deepClone } from '@core/utils';
  47 +import { deepClone, mergeDeep } from '@core/utils';
48 48
49 49 @Component({
50 50 selector: 'tb-entity-details-panel',
... ... @@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
280 280
281 281 saveEntity() {
282 282 if (this.detailsForm.valid) {
283   - const editingEntity = {...this.editingEntity, ...this.entityComponent.entityFormValue()};
  283 + const editingEntity = mergeDeep(this.editingEntity, this.entityComponent.entityFormValue());
284 284 this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
285 285 (entity) => {
286 286 this.entity = entity;
... ...
... ... @@ -14,7 +14,16 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  17 +import {
  18 + Component,
  19 + Injector,
  20 + Input,
  21 + OnDestroy,
  22 + OnInit,
  23 + StaticProvider,
  24 + ViewChild,
  25 + ViewContainerRef
  26 +} from '@angular/core';
18 27 import { TooltipPosition } from '@angular/material/tooltip';
19 28 import { IAliasController } from '@core/api/widget-api.models';
20 29 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
... ... @@ -28,7 +37,7 @@ import {
28 37 FiltersEditPanelComponent,
29 38 FiltersEditPanelData
30 39 } from '@home/components/filter/filters-edit-panel.component';
31   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  40 +import { ComponentPortal } from '@angular/cdk/portal';
32 41 import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component';
33 42 import { MatDialog } from '@angular/material/dialog';
34 43
... ... @@ -153,12 +162,12 @@ export class FiltersEditComponent implements OnInit, OnDestroy {
153 162 }
154 163 }
155 164
156   - private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): PortalInjector {
157   - const injectionTokens = new WeakMap<any, any>([
158   - [FILTER_EDIT_PANEL_DATA, data],
159   - [OverlayRef, overlayRef]
160   - ]);
161   - return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  165 + private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): Injector {
  166 + const providers: StaticProvider[] = [
  167 + {provide: FILTER_EDIT_PANEL_DATA, useValue: data},
  168 + {provide: OverlayRef, useValue: overlayRef}
  169 + ];
  170 + return Injector.create({parent: this.viewContainerRef.injector, providers});
162 171 }
163 172
164 173 private updateFiltersInfo() {
... ...
... ... @@ -67,15 +67,13 @@
67 67 </mat-form-field>
68 68 <section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value">
69 69 <ng-template [ngSwitchCase]="widgetActionType.openDashboard">
70   - <div fxLayout="column">
71   - <div class="mat-caption tb-required"
72   - style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
73   - <tb-dashboard-autocomplete
74   - formControlName="targetDashboardId"
75   - required
76   - [selectFirstDashboard]="true"
77   - ></tb-dashboard-autocomplete>
78   - </div>
  70 + <div class="mat-caption tb-required"
  71 + style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
  72 + <tb-dashboard-autocomplete
  73 + formControlName="targetDashboardId"
  74 + required
  75 + [selectFirstDashboard]="true"
  76 + ></tb-dashboard-autocomplete>
79 77 </ng-template>
80 78 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
81 79 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
... ... @@ -122,26 +120,24 @@
122 120 widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
123 121 widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
124 122 widgetActionFormGroup.get('type').value : ''">
125   - <div fxFlex fxLayout="column">
126   - <mat-checkbox formControlName="setEntityId">
127   - {{ 'widget-action.set-entity-from-widget' | translate }}
128   - </mat-checkbox>
129   - <mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"
130   - floatLabel="always"
131   - class="mat-block">
132   - <mat-label translate>alias.state-entity-parameter-name</mat-label>
133   - <input matInput
134   - placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
135   - formControlName="stateEntityParamName">
136   - </mat-form-field>
137   - </div>
  123 + <mat-checkbox formControlName="setEntityId">
  124 + {{ 'widget-action.set-entity-from-widget' | translate }}
  125 + </mat-checkbox>
  126 + <mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"
  127 + floatLabel="always"
  128 + class="mat-block">
  129 + <mat-label translate>alias.state-entity-parameter-name</mat-label>
  130 + <input matInput
  131 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  132 + formControlName="stateEntityParamName">
  133 + </mat-form-field>
138 134 </ng-template>
139 135 <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ?
140 136 widgetActionFormGroup.get('type').value : ''">
141 137 <mat-checkbox formControlName="openInSeparateDialog">
142 138 {{ 'widget-action.open-in-separate-dialog' | translate }}
143 139 </mat-checkbox>
144   - <section fxLayout="column" *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value">
  140 + <section *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value">
145 141 <mat-form-field class="mat-block">
146 142 <mat-label translate>widget-action.dialog-title</mat-label>
147 143 <input matInput formControlName="dialogTitle">
... ...
... ... @@ -27,7 +27,8 @@
27 27 </mat-progress-bar>
28 28 <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
29 29 <div class="dashboard-state-dialog-content" mat-dialog-content fxFlex fxLayout="column" style="padding: 8px;">
30   - <tb-dashboard-page [embedded]="true" [hideToolbar]="hideToolbar" [currentState]="state" [dashboard]="dashboard" style="width: 100%; height: 100%;"></tb-dashboard-page>
  30 + <tb-dashboard-page [embedded]="true" [syncStateWithQueryParam]="false" [hideToolbar]="hideToolbar"
  31 + [currentState]="state" [dashboard]="dashboard" style="width: 100%; height: 100%;"></tb-dashboard-page>
31 32 </div>
32 33 <div mat-dialog-actions fxLayoutAlign="end center">
33 34 <button mat-button color="primary"
... ...
... ... @@ -14,11 +14,22 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  17 +import {
  18 + Component,
  19 + forwardRef,
  20 + Inject,
  21 + Injector,
  22 + Input,
  23 + OnDestroy,
  24 + OnInit,
  25 + StaticProvider,
  26 + ViewChild,
  27 + ViewContainerRef
  28 +} from '@angular/core';
18 29 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 30 import { DOCUMENT } from '@angular/common';
20 31 import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
21   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  32 +import { ComponentPortal } from '@angular/cdk/portal';
22 33 import { MediaBreakpoints } from '@shared/models/constants';
23 34 import { BreakpointObserver } from '@angular/cdk/layout';
24 35 import { WINDOW } from '@core/services/window.service';
... ... @@ -140,12 +151,12 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
140 151 overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector));
141 152 }
142 153
143   - private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector {
144   - const injectionTokens = new WeakMap<any, any>([
145   - [LEGEND_CONFIG_PANEL_DATA, data],
146   - [OverlayRef, overlayRef]
147   - ]);
148   - return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  154 + private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): Injector {
  155 + const providers: StaticProvider[] = [
  156 + {provide: LEGEND_CONFIG_PANEL_DATA, useValue: data},
  157 + {provide: OverlayRef, useValue: overlayRef}
  158 + ];
  159 + return Injector.create({parent: this.viewContainerRef.injector, providers});
149 160 }
150 161
151 162 registerOnChange(fn: any): void {
... ...
... ... @@ -140,6 +140,7 @@
140 140 [length]="alarmsDatasource.total() | async"
141 141 [pageIndex]="pageLink.page"
142 142 [pageSize]="pageLink.pageSize"
143   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  143 + [pageSizeOptions]="pageSizeOptions"
  144 + showFirstLastButtons></mat-paginator>
144 145 </div>
145 146 </div>
... ...
... ... @@ -19,9 +19,11 @@ import {
19 19 Component,
20 20 ElementRef,
21 21 EventEmitter,
  22 + Injector,
22 23 Input,
23 24 NgZone,
24 25 OnInit,
  26 + StaticProvider,
25 27 ViewChild,
26 28 ViewContainerRef
27 29 } from '@angular/core';
... ... @@ -64,7 +66,7 @@ import {
64 66 widthStyle
65 67 } from '@home/components/widget/lib/table-widget.models';
66 68 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
67   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  69 +import { ComponentPortal } from '@angular/cdk/portal';
68 70 import {
69 71 DISPLAY_COLUMNS_PANEL_DATA,
70 72 DisplayColumnsPanelComponent,
... ... @@ -452,20 +454,26 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
452 454 };
453 455 });
454 456
455   - const injectionTokens = new WeakMap<any, any>([
456   - [DISPLAY_COLUMNS_PANEL_DATA, {
457   - columns,
458   - columnsUpdated: (newColumns) => {
459   - this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
460   - if (this.enableSelection) {
461   - this.displayedColumns.unshift('select');
  457 + const providers: StaticProvider[] = [
  458 + {
  459 + provide: DISPLAY_COLUMNS_PANEL_DATA,
  460 + useValue: {
  461 + columns,
  462 + columnsUpdated: (newColumns) => {
  463 + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
  464 + if (this.enableSelection) {
  465 + this.displayedColumns.unshift('select');
  466 + }
  467 + this.displayedColumns.push('actions');
462 468 }
463   - this.displayedColumns.push('actions');
464   - }
465   - } as DisplayColumnsPanelData],
466   - [OverlayRef, overlayRef]
467   - ]);
468   - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  469 + } as DisplayColumnsPanelData
  470 + },
  471 + {
  472 + provide: OverlayRef,
  473 + useValue: overlayRef
  474 + }
  475 + ];
  476 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
469 477 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
470 478 this.viewContainerRef, injector));
471 479 this.ctx.detectChanges();
... ... @@ -492,15 +500,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
492 500 overlayRef.backdropClick().subscribe(() => {
493 501 overlayRef.dispose();
494 502 });
495   - const injectionTokens = new WeakMap<any, any>([
496   - [ALARM_FILTER_PANEL_DATA, {
497   - statusList: this.pageLink.statusList,
498   - severityList: this.pageLink.severityList,
499   - typeList: this.pageLink.typeList
500   - } as AlarmFilterPanelData],
501   - [OverlayRef, overlayRef]
502   - ]);
503   - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  503 + const providers: StaticProvider[] = [
  504 + {
  505 + provide: ALARM_FILTER_PANEL_DATA,
  506 + useValue: {
  507 + statusList: this.pageLink.statusList,
  508 + severityList: this.pageLink.severityList,
  509 + typeList: this.pageLink.typeList
  510 + } as AlarmFilterPanelData
  511 + },
  512 + {
  513 + provide: OverlayRef,
  514 + useValue: overlayRef
  515 + }
  516 + ];
  517 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
504 518 const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent,
505 519 this.viewContainerRef, injector));
506 520 componentRef.onDestroy(() => {
... ...
... ... @@ -18,9 +18,11 @@ import {
18 18 Component,
19 19 Inject,
20 20 InjectionToken,
  21 + Injector,
21 22 Input,
22 23 OnDestroy,
23 24 OnInit,
  25 + StaticProvider,
24 26 ViewChild,
25 27 ViewContainerRef,
26 28 ViewEncapsulation
... ... @@ -41,7 +43,7 @@ import {
41 43 import { KeyValue } from '@angular/common';
42 44 import * as _moment from 'moment';
43 45 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
44   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  46 +import { ComponentPortal } from '@angular/cdk/portal';
45 47 import { MatSelect } from '@angular/material/select';
46 48 import { Subscription } from 'rxjs';
47 49 import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.models';
... ... @@ -142,18 +144,24 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements
142 144 overlayRef.backdropClick().subscribe(() => {
143 145 overlayRef.dispose();
144 146 });
145   - const injectionTokens = new WeakMap<any, any>([
146   - [DATE_RANGE_NAVIGATOR_PANEL_DATA, {
147   - model: cloneDateRangeNavigatorModel(this.advancedModel),
148   - settings: this.settings,
149   - onChange: model => {
150   - this.advancedModel = model;
151   - this.triggerChange();
152   - }
153   - } as DateRangeNavigatorPanelData],
154   - [OverlayRef, overlayRef]
155   - ]);
156   - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  147 + const providers: StaticProvider[] = [
  148 + {
  149 + provide: DATE_RANGE_NAVIGATOR_PANEL_DATA,
  150 + useValue: {
  151 + model: cloneDateRangeNavigatorModel(this.advancedModel),
  152 + settings: this.settings,
  153 + onChange: model => {
  154 + this.advancedModel = model;
  155 + this.triggerChange();
  156 + }
  157 + } as DateRangeNavigatorPanelData
  158 + },
  159 + {
  160 + provide: OverlayRef,
  161 + useValue: overlayRef
  162 + }
  163 + ];
  164 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
157 165 overlayRef.attach(new ComponentPortal(DateRangeNavigatorPanelComponent,
158 166 this.viewContainerRef, injector));
159 167 this.ctx.detectChanges();
... ...
... ... @@ -99,6 +99,7 @@
99 99 [length]="entityDatasource.total() | async"
100 100 [pageIndex]="pageLink.page"
101 101 [pageSize]="pageLink.pageSize"
102   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  102 + [pageSizeOptions]="pageSizeOptions"
  103 + showFirstLastButtons></mat-paginator>
103 104 </div>
104 105 </div>
... ...
... ... @@ -18,9 +18,11 @@ import {
18 18 AfterViewInit,
19 19 Component,
20 20 ElementRef,
  21 + Injector,
21 22 Input,
22 23 NgZone,
23 24 OnInit,
  25 + StaticProvider,
24 26 ViewChild,
25 27 ViewContainerRef
26 28 } from '@angular/core';
... ... @@ -70,7 +72,7 @@ import {
70 72 widthStyle
71 73 } from '@home/components/widget/lib/table-widget.models';
72 74 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
73   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  75 +import { ComponentPortal } from '@angular/cdk/portal';
74 76 import {
75 77 DISPLAY_COLUMNS_PANEL_DATA,
76 78 DisplayColumnsPanelComponent,
... ... @@ -422,17 +424,23 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
422 424 };
423 425 });
424 426
425   - const injectionTokens = new WeakMap<any, any>([
426   - [DISPLAY_COLUMNS_PANEL_DATA, {
427   - columns,
428   - columnsUpdated: (newColumns) => {
429   - this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
430   - this.displayedColumns.push('actions');
431   - }
432   - } as DisplayColumnsPanelData],
433   - [OverlayRef, overlayRef]
434   - ]);
435   - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  427 + const providers: StaticProvider[] = [
  428 + {
  429 + provide: DISPLAY_COLUMNS_PANEL_DATA,
  430 + useValue: {
  431 + columns,
  432 + columnsUpdated: (newColumns) => {
  433 + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
  434 + this.displayedColumns.push('actions');
  435 + }
  436 + } as DisplayColumnsPanelData
  437 + },
  438 + {
  439 + provide: OverlayRef,
  440 + useValue: overlayRef
  441 + }
  442 + ];
  443 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
436 444 overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
437 445 this.viewContainerRef, injector));
438 446 this.ctx.detectChanges();
... ...
... ... @@ -175,7 +175,7 @@ export class TbFlot {
175 175 autoHighlight: this.tooltipIndividual === true,
176 176 markings: []
177 177 },
178   - selection : { mode : ctx.isMobile ? null : 'x' },
  178 + selection : { mode : 'x' },
179 179 legend : {
180 180 show: false
181 181 }
... ... @@ -702,7 +702,7 @@ export class TbFlot {
702 702 }
703 703
704 704 public checkMouseEvents() {
705   - const enabled = !this.ctx.isMobile && !this.ctx.isEdit;
  705 + const enabled = !this.ctx.isEdit;
706 706 if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) {
707 707 this.mouseEventsEnabled = enabled;
708 708 if (this.$element) {
... ...
... ... @@ -626,10 +626,14 @@ export default abstract class LeafletMap {
626 626 }
627 627 this.points = new FeatureGroup();
628 628 }
  629 + let pointColor = this.options.pointColor;
629 630 for (const pointsList of pointsData) {
630 631 pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => {
  632 + if (this.options.useColorPointFunction) {
  633 + pointColor = safeExecute(this.options.colorPointFunction, [data, pointsData, data.dsIndex]);
  634 + }
631 635 const point = L.circleMarker(this.convertPosition(data), {
632   - color: this.options.pointColor,
  636 + color: pointColor,
633 637 radius: this.options.pointSize
634 638 });
635 639 if (!this.options.pointTooltipOnRightPanel) {
... ...
... ... @@ -201,6 +201,8 @@ export type TripAnimationSettings = {
201 201 pointAsAnchorFunction: GenericFunction;
202 202 tooltipFunction: GenericFunction;
203 203 labelFunction: GenericFunction;
  204 + useColorPointFunction: boolean;
  205 + colorPointFunction: GenericFunction;
204 206 };
205 207
206 208 export type actionsHandler = ($event: Event, datasource: Datasource) => void;
... ...
... ... @@ -301,6 +301,7 @@ export class MapWidgetController implements MapWidgetInterface {
301 301 labelFunction: parseFunction(settings.labelFunction, functionParams),
302 302 tooltipFunction: parseFunction(settings.tooltipFunction, functionParams),
303 303 colorFunction: parseFunction(settings.colorFunction, functionParams),
  304 + colorPointFunction: parseFunction(settings.colorPointFunction, functionParams),
304 305 polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams),
305 306 polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams),
306 307 markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']),
... ...
... ... @@ -871,6 +871,15 @@ export const pointSchema =
871 871 title: 'Point color',
872 872 type: 'string'
873 873 },
  874 + useColorPointFunction: {
  875 + title: 'Use color point function',
  876 + type: 'boolean',
  877 + default: false
  878 + },
  879 + colorPointFunction: {
  880 + title: 'Color point function: f(data, dsData, dsIndex)',
  881 + type: 'string'
  882 + },
874 883 pointSize: {
875 884 title: 'Point size (px)',
876 885 type: 'number',
... ... @@ -899,6 +908,11 @@ export const pointSchema =
899 908 key: 'pointColor',
900 909 type: 'color'
901 910 },
  911 + 'useColorPointFunction',
  912 + {
  913 + key: 'colorPointFunction',
  914 + type: 'javascript'
  915 + },
902 916 'pointSize',
903 917 'usePointAsAnchor',
904 918 {
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<a mat-raised-button color="primary" class="tb-nav-button" href="{{settings.path}}" (click)="navigate($event, settings.path)">
  19 + <mat-icon class="material-icons tb-mat-96">{{settings.icon}}</mat-icon>
  20 + <span>{{translatedName}}</span>
  21 +</a>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 + width: 100%;
  19 + height: 100%;
  20 +}
  21 +
  22 +:host ::ng-deep {
  23 + .tb-nav-button {
  24 + width: 100%;
  25 + height: 100%;
  26 + &:hover {
  27 + border-bottom: none;
  28 + }
  29 + &:focus {
  30 + border-bottom: none;
  31 + }
  32 + .mat-button-wrapper {
  33 + width: 100%;
  34 + height: 100%;
  35 + display: flex;
  36 + flex-direction: column;
  37 + align-items: center;
  38 + mat-icon {
  39 + margin: auto;
  40 + }
  41 + span {
  42 + height: 18px;
  43 + min-height: 36px;
  44 + max-height: 36px;
  45 + padding: 0 0 20px 0;
  46 + margin: auto;
  47 + font-size: 18px;
  48 + font-weight: 400;
  49 + line-height: 18px;
  50 + white-space: normal;
  51 + }
  52 + }
  53 + &.mat-raised-button.mat-primary {
  54 + .mat-ripple-element {
  55 + opacity: 0.3;
  56 + background-color: rgba(255, 255, 255, 0.3);
  57 + }
  58 + }
  59 + }
  60 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { PageComponent } from '@shared/components/page.component';
  18 +import { Component, Input, NgZone, OnInit } from '@angular/core';
  19 +import { WidgetContext } from '@home/models/widget-component.models';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { Router } from '@angular/router';
  23 +import { UtilsService } from '@core/services/utils.service';
  24 +
  25 +interface NavigationCardWidgetSettings {
  26 + name: string;
  27 + icon: string;
  28 + path: string;
  29 +}
  30 +
  31 +@Component({
  32 + selector: 'tb-navigation-card-widget',
  33 + templateUrl: './navigation-card-widget.component.html',
  34 + styleUrls: ['./navigation-card-widget.component.scss']
  35 +})
  36 +export class NavigationCardWidgetComponent extends PageComponent implements OnInit {
  37 +
  38 + settings: NavigationCardWidgetSettings;
  39 +
  40 + translatedName: string;
  41 +
  42 + @Input()
  43 + ctx: WidgetContext;
  44 +
  45 + constructor(protected store: Store<AppState>,
  46 + private utils: UtilsService,
  47 + private ngZone: NgZone,
  48 + private router: Router) {
  49 + super(store);
  50 + }
  51 +
  52 + ngOnInit(): void {
  53 + this.ctx.$scope.navigationCardWidget = this;
  54 + this.settings = this.ctx.settings;
  55 + this.translatedName = this.utils.customTranslation(this.settings.name, this.settings.name);
  56 + }
  57 +
  58 +
  59 + navigate($event: Event, path: string) {
  60 + $event.preventDefault();
  61 + this.ngZone.run(() => {
  62 + this.router.navigateByUrl(path);
  63 + });
  64 + }
  65 +
  66 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-grid-list *ngIf="cols" class="tb-navigation-cards" [cols]="cols" rowHeight="280px">
  19 + <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of showHomeSections$| async">
  20 + <mat-card style="width: 100%;">
  21 + <mat-card-title>
  22 + <span translate class="mat-headline">{{section.name}}</span>
  23 + </mat-card-title>
  24 + <mat-card-content>
  25 + <mat-grid-list rowHeight="170px" [cols]="sectionPlaces(section).length">
  26 + <mat-grid-tile *ngFor="let place of sectionPlaces(section)">
  27 + <a mat-raised-button color="primary" class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
  28 + <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
  29 + <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
  30 + <span translate>{{place.name}}</span>
  31 + </a>
  32 + </mat-grid-tile>
  33 + </mat-grid-list>
  34 + </mat-card-content>
  35 + </mat-card>
  36 + </mat-grid-tile>
  37 +</mat-grid-list>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import '../../../../../../scss/constants';
  17 +
  18 +:host {
  19 + width: 100%;
  20 + height: 100%;
  21 +}
  22 +
  23 +:host ::ng-deep {
  24 + .tb-navigation-cards {
  25 + .mat-headline {
  26 + font-size: 20px;
  27 + @media #{$mat-gt-xmd} {
  28 + font-size: 24px;
  29 + }
  30 + }
  31 + mat-card {
  32 + padding: 0;
  33 + margin: 8px;
  34 + mat-card-title {
  35 + margin: 0;
  36 + padding: 24px 16px 16px;
  37 + }
  38 + mat-card-title+mat-card-content {
  39 + padding-top: 0;
  40 + }
  41 + mat-card-content {
  42 + padding: 16px;
  43 + }
  44 + }
  45 + .tb-card-button {
  46 + width: 100%;
  47 + height: 100%;
  48 + max-width: 240px;
  49 + &:hover {
  50 + border-bottom: none;
  51 + }
  52 + &:focus {
  53 + border-bottom: none;
  54 + }
  55 + .mat-button-wrapper {
  56 + width: 100%;
  57 + height: 100%;
  58 + display: flex;
  59 + flex-direction: column;
  60 + align-items: center;
  61 + mat-icon {
  62 + margin: auto;
  63 + }
  64 + span {
  65 + height: 18px;
  66 + min-height: 36px;
  67 + max-height: 36px;
  68 + padding: 0 0 20px 0;
  69 + margin: auto;
  70 + font-size: 18px;
  71 + font-weight: 400;
  72 + line-height: 18px;
  73 + white-space: normal;
  74 + }
  75 + }
  76 + &.mat-raised-button.mat-primary {
  77 + .mat-ripple-element {
  78 + opacity: 0.3;
  79 + background-color: rgba(255, 255, 255, 0.3);
  80 + }
  81 + }
  82 + }
  83 + }
  84 +}
  85 +
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { PageComponent } from '@shared/components/page.component';
  18 +import { Component, Input, NgZone, OnInit } from '@angular/core';
  19 +import { WidgetContext } from '@home/models/widget-component.models';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { MenuService } from '@core/services/menu.service';
  23 +import { HomeSection, HomeSectionPlace } from '@core/services/menu.models';
  24 +import { Router } from '@angular/router';
  25 +import { map } from 'rxjs/operators';
  26 +
  27 +interface NavigationCardsWidgetSettings {
  28 + filterType: 'all' | 'include' | 'exclude';
  29 + filter: string[];
  30 +}
  31 +
  32 +@Component({
  33 + selector: 'tb-navigation-cards-widget',
  34 + templateUrl: './navigation-cards-widget.component.html',
  35 + styleUrls: ['./navigation-cards-widget.component.scss']
  36 +})
  37 +export class NavigationCardsWidgetComponent extends PageComponent implements OnInit {
  38 +
  39 + homeSections$ = this.menuService.homeSections();
  40 + showHomeSections$ = this.homeSections$.pipe(
  41 + map((sections) => {
  42 + return sections.filter((section) => this.sectionPlaces(section).length > 0);
  43 + })
  44 + );
  45 +
  46 + cols = null;
  47 +
  48 + settings: NavigationCardsWidgetSettings;
  49 +
  50 + @Input()
  51 + ctx: WidgetContext;
  52 +
  53 + constructor(protected store: Store<AppState>,
  54 + private menuService: MenuService,
  55 + private ngZone: NgZone,
  56 + private router: Router) {
  57 + super(store);
  58 + }
  59 +
  60 + ngOnInit(): void {
  61 + this.ctx.$scope.navigationCardsWidget = this;
  62 + this.settings = this.ctx.settings;
  63 + }
  64 +
  65 + resize() {
  66 + this.updateColumnCount();
  67 + }
  68 +
  69 + private updateColumnCount() {
  70 + this.cols = 2;
  71 + const width = this.ctx.width;
  72 + if (width >= 1280) {
  73 + this.cols = 3;
  74 + if (width >= 1920) {
  75 + this.cols = 4;
  76 + }
  77 + }
  78 + this.ctx.detectChanges();
  79 + }
  80 +
  81 + navigate($event: Event, path: string) {
  82 + $event.preventDefault();
  83 + this.ngZone.run(() => {
  84 + this.router.navigateByUrl(path);
  85 + });
  86 + }
  87 +
  88 + sectionPlaces(section: HomeSection): HomeSectionPlace[] {
  89 + return section && section.places ? section.places.filter((place) => this.filterPlace(place)) : [];
  90 + }
  91 +
  92 + private filterPlace(place: HomeSectionPlace): boolean {
  93 + if (this.settings.filterType === 'include') {
  94 + return this.settings.filter.includes(place.path);
  95 + } else if (this.settings.filterType === 'exclude') {
  96 + return !this.settings.filter.includes(place.path);
  97 + }
  98 + return true;
  99 + }
  100 +
  101 + sectionColspan(section: HomeSection): number {
  102 + if (this.ctx.width >= 960) {
  103 + let colspan = this.cols;
  104 + const places = this.sectionPlaces(section);
  105 + if (places.length <= colspan) {
  106 + colspan = places.length;
  107 + }
  108 + return colspan;
  109 + } else {
  110 + return 2;
  111 + }
  112 + }
  113 +
  114 +}
... ...
... ... @@ -104,7 +104,8 @@
104 104 [length]="source.timeseriesDatasource.total() | async"
105 105 [pageIndex]="source.pageLink.page"
106 106 [pageSize]="source.pageLink.pageSize"
107   - [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  107 + [pageSizeOptions]="pageSizeOptions"
  108 + showFirstLastButtons></mat-paginator>
108 109 </mat-tab>
109 110 </mat-tab-group>
110 111 </div>
... ...
... ... @@ -210,7 +210,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
210 210 this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
211 211 this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false;
212 212 this.showTimestamp = this.settings.showTimestamp !== false;
213   - this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss';
  213 + this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.SSS';
214 214
215 215 const pageSize = this.settings.defaultPageSize;
216 216 if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
... ...
... ... @@ -112,6 +112,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
112 112 this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']);
113 113 this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']);
114 114 this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']);
  115 + this.settings.colorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']);
115 116 this.normalizationStep = this.settings.normalizationStep;
116 117 const subscription = this.ctx.defaultSubscription;
117 118 subscription.callbacks.onDataUpdated = () => {
... ...
... ... @@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen
35 35 import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component';
36 36 import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
37 37 import { ImportExportService } from '@home/components/import-export/import-export.service';
  38 +import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
  39 +import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
38 40
39 41 @NgModule({
40 42 declarations:
... ... @@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
50 52 MultipleInputWidgetComponent,
51 53 TripAnimationComponent,
52 54 PhotoCameraInputWidgetComponent,
53   - GatewayFormComponent
  55 + GatewayFormComponent,
  56 + NavigationCardsWidgetComponent,
  57 + NavigationCardWidgetComponent
54 58 ],
55 59 imports: [
56 60 CommonModule,
... ... @@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
68 72 MultipleInputWidgetComponent,
69 73 TripAnimationComponent,
70 74 PhotoCameraInputWidgetComponent,
71   - GatewayFormComponent
  75 + GatewayFormComponent,
  76 + NavigationCardsWidgetComponent,
  77 + NavigationCardWidgetComponent
72 78 ],
73 79 providers: [
74 80 CustomDialogService,
... ...
... ... @@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
32 32 import { OAuth2Service } from '@core/http/oauth2.service';
33 33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module';
34 34 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
  35 +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
35 36
36 37 @Injectable()
37 38 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
... ... @@ -48,7 +49,7 @@ const routes: Routes = [
48 49 {
49 50 path: 'settings',
50 51 data: {
51   - auth: [Authority.SYS_ADMIN],
  52 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
52 53 breadcrumb: {
53 54 label: 'admin.system-settings',
54 55 icon: 'settings'
... ... @@ -57,8 +58,13 @@ const routes: Routes = [
57 58 children: [
58 59 {
59 60 path: '',
60   - redirectTo: 'general',
61   - pathMatch: 'full'
  61 + data: {
  62 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  63 + redirectTo: {
  64 + SYS_ADMIN: '/settings/general',
  65 + TENANT_ADMIN: '/settings/home'
  66 + }
  67 + }
62 68 },
63 69 {
64 70 path: 'general',
... ... @@ -127,6 +133,19 @@ const routes: Routes = [
127 133 resolve: {
128 134 loginProcessingUrl: OAuth2LoginProcessingUrlResolver
129 135 }
  136 + },
  137 + {
  138 + path: 'home',
  139 + component: HomeSettingsComponent,
  140 + canDeactivate: [ConfirmOnExitGuard],
  141 + data: {
  142 + auth: [Authority.TENANT_ADMIN],
  143 + title: 'admin.home-settings',
  144 + breadcrumb: {
  145 + label: 'admin.home-settings',
  146 + icon: 'settings_applications'
  147 + }
  148 + }
130 149 }
131 150 ]
132 151 }
... ...
... ... @@ -26,6 +26,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m
26 26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
27 27 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
28 28 import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
  29 +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
29 30
30 31 @NgModule({
31 32 declarations:
... ... @@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial
35 36 SmsProviderComponent,
36 37 SendTestSmsDialogComponent,
37 38 SecuritySettingsComponent,
38   - OAuth2SettingsComponent
  39 + OAuth2SettingsComponent,
  40 + HomeSettingsComponent
39 41 ],
40 42 imports: [
41 43 CommonModule,
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.home-settings</span>
  23 + </div>
  24 + </mat-card-title>
  25 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  26 + </mat-progress-bar>
  27 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  28 + <mat-card-content style="padding-top: 16px;">
  29 + <form [formGroup]="homeSettings" (ngSubmit)="save()">
  30 + <fieldset [disabled]="isLoading$ | async">
  31 + <section class="tb-default-dashboard" fxFlex fxLayout="column">
  32 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  33 + <tb-dashboard-autocomplete
  34 + fxFlex
  35 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  36 + formControlName="dashboardId"
  37 + [dashboardsScope]="'tenant'"
  38 + [selectFirstDashboard]="false"
  39 + ></tb-dashboard-autocomplete>
  40 + <mat-checkbox fxFlex formControlName="hideDashboardToolbar">
  41 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  42 + </mat-checkbox>
  43 + </section>
  44 + </section>
  45 + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
  46 + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || homeSettings.invalid || !homeSettings.dirty"
  47 + type="submit">{{'action.save' | translate}}
  48 + </button>
  49 + </div>
  50 + </fieldset>
  51 + </form>
  52 + </mat-card-content>
  53 + </mat-card>
  54 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@import "../../../../../scss/constants";
  18 +
  19 +:host {
  20 + .tb-default-dashboard {
  21 + tb-dashboard-autocomplete {
  22 + @media #{$mat-gt-sm} {
  23 + padding-right: 12px;
  24 + }
  25 +
  26 + @media #{$mat-lt-md} {
  27 + padding-bottom: 12px;
  28 + }
  29 + }
  30 + mat-checkbox {
  31 + @media #{$mat-gt-sm} {
  32 + margin-top: 16px;
  33 + }
  34 + }
  35 + }
  36 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { Router } from '@angular/router';
  22 +import { FormBuilder, FormGroup } from '@angular/forms';
  23 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  24 +import { DashboardService } from '@core/http/dashboard.service';
  25 +import { HomeDashboardInfo } from '@shared/models/dashboard.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
  27 +import { DashboardId } from '@shared/models/id/dashboard-id';
  28 +
  29 +@Component({
  30 + selector: 'tb-home-settings',
  31 + templateUrl: './home-settings.component.html',
  32 + styleUrls: ['./home-settings.component.scss', './settings-card.scss']
  33 +})
  34 +export class HomeSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
  35 +
  36 + homeSettings: FormGroup;
  37 +
  38 + constructor(protected store: Store<AppState>,
  39 + private router: Router,
  40 + private dashboardService: DashboardService,
  41 + public fb: FormBuilder) {
  42 + super(store);
  43 + }
  44 +
  45 + ngOnInit() {
  46 + this.homeSettings = this.fb.group({
  47 + dashboardId: [null],
  48 + hideDashboardToolbar: [true]
  49 + });
  50 + this.dashboardService.getTenantHomeDashboardInfo().subscribe(
  51 + (homeDashboardInfo) => {
  52 + this.setHomeDashboardInfo(homeDashboardInfo);
  53 + }
  54 + );
  55 + }
  56 +
  57 + save(): void {
  58 + const strDashboardId = this.homeSettings.get('dashboardId').value;
  59 + const dashboardId: DashboardId = strDashboardId ? new DashboardId(strDashboardId) : null;
  60 + const hideDashboardToolbar = this.homeSettings.get('hideDashboardToolbar').value;
  61 + const homeDashboardInfo: HomeDashboardInfo = {
  62 + dashboardId,
  63 + hideDashboardToolbar
  64 + };
  65 + this.dashboardService.setTenantHomeDashboardInfo(homeDashboardInfo).subscribe(
  66 + () => {
  67 + this.setHomeDashboardInfo(homeDashboardInfo);
  68 + }
  69 + );
  70 + }
  71 +
  72 + confirmForm(): FormGroup {
  73 + return this.homeSettings;
  74 + }
  75 +
  76 + private setHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo) {
  77 + this.homeSettings.reset({
  78 + dashboardId: homeDashboardInfo?.dashboardId?.id,
  79 + hideDashboardToolbar: isDefinedAndNotNull(homeDashboardInfo?.hideDashboardToolbar) ?
  80 + homeDashboardInfo?.hideDashboardToolbar : true
  81 + });
  82 + }
  83 +
  84 +}
... ...
... ... @@ -72,6 +72,21 @@
72 72 <mat-label translate>customer.description</mat-label>
73 73 <textarea matInput formControlName="description" rows="2"></textarea>
74 74 </mat-form-field>
  75 + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
  76 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  77 + <tb-dashboard-autocomplete
  78 + fxFlex
  79 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  80 + formControlName="homeDashboardId"
  81 + [dashboardsScope]="'customer'"
  82 + [customerId]="entity?.id.id"
  83 + [selectFirstDashboard]="false"
  84 + ></tb-dashboard-autocomplete>
  85 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  86 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  87 + </mat-checkbox>
  88 + </section>
  89 + </section>
75 90 </div>
76 91 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
77 92 </fieldset>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + .tb-default-dashboard {
  20 + tb-dashboard-autocomplete {
  21 + @media #{$mat-gt-sm} {
  22 + padding-right: 12px;
  23 + }
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding-bottom: 12px;
  27 + }
  28 + }
  29 + mat-checkbox {
  30 + @media #{$mat-gt-sm} {
  31 + margin-top: 16px;
  32 + }
  33 + }
  34 + }
  35 +}
... ...
... ... @@ -23,10 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
23 23 import { TranslateService } from '@ngx-translate/core';
24 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 28 @Component({
28 29 selector: 'tb-customer',
29   - templateUrl: './customer.component.html'
  30 + templateUrl: './customer.component.html',
  31 + styleUrls: ['./customer.component.scss']
30 32 })
31 33 export class CustomerComponent extends ContactBasedComponent<Customer> {
32 34
... ... @@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
54 56 title: [entity ? entity.title : '', [Validators.required]],
55 57 additionalInfo: this.fb.group(
56 58 {
57   - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
  59 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  60 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  61 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  62 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
58 63 }
59 64 )
60 65 }
... ... @@ -65,6 +70,11 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
65 70 this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic;
66 71 this.entityForm.patchValue({title: entity.title});
67 72 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  73 + this.entityForm.patchValue({additionalInfo:
  74 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  75 + this.entityForm.patchValue({additionalInfo:
  76 + {homeDashboardHideToolbar: entity.additionalInfo &&
  77 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
68 78 }
69 79
70 80 onCustomerIdCopied(event) {
... ...
... ... @@ -14,11 +14,25 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { NgModule } from '@angular/core';
18   -import { RouterModule, Routes } from '@angular/router';
  17 +import { Injectable, NgModule } from '@angular/core';
  18 +import { Resolve, RouterModule, Routes } from '@angular/router';
19 19
20 20 import { HomeLinksComponent } from './home-links.component';
21 21 import { Authority } from '@shared/models/authority.enum';
  22 +import { Observable } from 'rxjs';
  23 +import { HomeDashboard } from '@shared/models/dashboard.models';
  24 +import { DashboardService } from '@core/http/dashboard.service';
  25 +
  26 +@Injectable()
  27 +export class HomeDashboardResolver implements Resolve<HomeDashboard> {
  28 +
  29 + constructor(private dashboardService: DashboardService) {
  30 + }
  31 +
  32 + resolve(): Observable<HomeDashboard> {
  33 + return this.dashboardService.getHomeDashboard();
  34 + }
  35 +}
22 36
23 37 const routes: Routes = [
24 38 {
... ... @@ -31,12 +45,18 @@ const routes: Routes = [
31 45 label: 'home.home',
32 46 icon: 'home'
33 47 }
  48 + },
  49 + resolve: {
  50 + homeDashboard: HomeDashboardResolver
34 51 }
35 52 }
36 53 ];
37 54
38 55 @NgModule({
39 56 imports: [RouterModule.forChild(routes)],
40   - exports: [RouterModule]
  57 + exports: [RouterModule],
  58 + providers: [
  59 + HomeDashboardResolver
  60 + ]
41 61 })
42 62 export class HomeLinksRoutingModule { }
... ...
... ... @@ -15,23 +15,26 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">
19   - <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">
20   - <mat-card style="width: 100%;">
21   - <mat-card-title>
22   - <span translate class="mat-headline">{{section.name}}</span>
23   - </mat-card-title>
24   - <mat-card-content>
25   - <mat-grid-list rowHeight="170px" [cols]="section.places.length">
26   - <mat-grid-tile *ngFor="let place of section.places">
27   - <a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
28   - <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
29   - <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
30   - <span translate>{{place.name}}</span>
31   - </a>
32   - </mat-grid-tile>
33   - </mat-grid-list>
34   - </mat-card-content>
35   - </mat-card>
36   - </mat-grid-tile>
37   -</mat-grid-list>
  18 +<tb-dashboard-page *ngIf="homeDashboard; else homeLinks" [embedded]="true" [dashboard]="homeDashboard" [hideToolbar]="homeDashboard.hideDashboardToolbar"></tb-dashboard-page>
  19 +<ng-template #homeLinks>
  20 + <mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">
  21 + <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">
  22 + <mat-card style="width: 100%;">
  23 + <mat-card-title>
  24 + <span translate class="mat-headline">{{section.name}}</span>
  25 + </mat-card-title>
  26 + <mat-card-content>
  27 + <mat-grid-list rowHeight="170px" [cols]="section.places.length">
  28 + <mat-grid-tile *ngFor="let place of section.places">
  29 + <a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
  30 + <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
  31 + <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
  32 + <span translate>{{place.name}}</span>
  33 + </a>
  34 + </mat-grid-tile>
  35 + </mat-grid-list>
  36 + </mat-card-content>
  37 + </mat-card>
  38 + </mat-grid-tile>
  39 + </mat-grid-list>
  40 +</ng-template>
... ...
... ... @@ -15,6 +15,11 @@
15 15 */
16 16 @import '../../../../../scss/constants';
17 17
  18 +:host {
  19 + width: 100%;
  20 + height: 100%;
  21 +}
  22 +
18 23 :host ::ng-deep {
19 24 .tb-home-links {
20 25 .mat-headline {
... ...
... ... @@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service';
19 19 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
20 20 import { MediaBreakpoints } from '@shared/models/constants';
21 21 import { HomeSection } from '@core/services/menu.models';
  22 +import { ActivatedRoute } from '@angular/router';
  23 +import { HomeDashboard } from '@shared/models/dashboard.models';
22 24
23 25 @Component({
24 26 selector: 'tb-home-links',
... ... @@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit {
31 33
32 34 cols = 2;
33 35
  36 + homeDashboard: HomeDashboard = this.route.snapshot.data.homeDashboard;
  37 +
34 38 constructor(private menuService: MenuService,
35   - public breakpointObserver: BreakpointObserver) {
  39 + public breakpointObserver: BreakpointObserver,
  40 + private route: ActivatedRoute) {
36 41 }
37 42
38 43 ngOnInit() {
39   - this.updateColumnCount();
40   - this.breakpointObserver
41   - .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])
42   - .subscribe((state: BreakpointState) => this.updateColumnCount());
  44 + if (!this.homeDashboard) {
  45 + this.updateColumnCount();
  46 + this.breakpointObserver
  47 + .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])
  48 + .subscribe((state: BreakpointState) => this.updateColumnCount());
  49 + }
43 50 }
44 51
45 52 private updateColumnCount() {
... ...
... ... @@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common';
20 20 import { HomeLinksRoutingModule } from './home-links-routing.module';
21 21 import { HomeLinksComponent } from './home-links.component';
22 22 import { SharedModule } from '@app/shared/shared.module';
  23 +import { HomeComponentsModule } from '@home/components/home-components.module';
23 24
24 25 @NgModule({
25 26 declarations:
... ... @@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module';
29 30 imports: [
30 31 CommonModule,
31 32 SharedModule,
  33 + HomeComponentsModule,
32 34 HomeLinksRoutingModule
33 35 ]
34 36 })
... ...
... ... @@ -63,6 +63,20 @@
63 63 </mat-option>
64 64 </mat-select>
65 65 </mat-form-field>
  66 + <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row">
  67 + <tb-dashboard-autocomplete
  68 + fxFlex
  69 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  70 + formControlName="homeDashboardId"
  71 + [dashboardsScope]="user?.authority === authorities.TENANT_ADMIN ? 'tenant' : 'customer'"
  72 + [tenantId]="user?.tenantId?.id"
  73 + [customerId]="user?.customerId?.id"
  74 + [selectFirstDashboard]="false"
  75 + ></tb-dashboard-autocomplete>
  76 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  77 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  78 + </mat-checkbox>
  79 + </section>
66 80 <div fxLayout="row" style="padding-bottom: 16px;">
67 81 <button mat-button mat-raised-button color="primary"
68 82 type="button"
... ...
... ... @@ -38,5 +38,21 @@
38 38 font-size: 16px;
39 39 font-weight: 400;
40 40 }
  41 + .tb-home-dashboard {
  42 + tb-dashboard-autocomplete {
  43 + @media #{$mat-gt-sm} {
  44 + padding-right: 12px;
  45 + }
  46 +
  47 + @media #{$mat-lt-md} {
  48 + padding-bottom: 12px;
  49 + }
  50 + }
  51 + mat-checkbox {
  52 + @media #{$mat-gt-sm} {
  53 + margin-top: 16px;
  54 + }
  55 + }
  56 + }
41 57 }
42 58 }
... ...
... ... @@ -32,6 +32,7 @@ import { MatDialog } from '@angular/material/dialog';
32 32 import { DialogService } from '@core/services/dialog.service';
33 33 import { AuthService } from '@core/auth/auth.service';
34 34 import { ActivatedRoute } from '@angular/router';
  35 +import { isDefinedAndNotNull } from '@core/utils';
35 36
36 37 @Component({
37 38 selector: 'tb-profile',
... ... @@ -66,7 +67,9 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
66 67 email: ['', [Validators.required, Validators.email]],
67 68 firstName: [''],
68 69 lastName: [''],
69   - language: ['']
  70 + language: [''],
  71 + homeDashboardId: [null],
  72 + homeDashboardHideToolbar: [true]
70 73 });
71 74 }
72 75
... ... @@ -76,6 +79,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
76 79 this.user.additionalInfo = {};
77 80 }
78 81 this.user.additionalInfo.lang = this.profile.get('language').value;
  82 + this.user.additionalInfo.homeDashboardId = this.profile.get('homeDashboardId').value;
  83 + this.user.additionalInfo.homeDashboardHideToolbar = this.profile.get('homeDashboardHideToolbar').value;
79 84 this.userService.saveUser(this.user).subscribe(
80 85 (user) => {
81 86 this.userLoaded(user);
... ... @@ -106,12 +111,23 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
106 111 this.user = user;
107 112 this.profile.reset(user);
108 113 let lang;
109   - if (user.additionalInfo && user.additionalInfo.lang) {
110   - lang = user.additionalInfo.lang;
111   - } else {
  114 + let homeDashboardId;
  115 + let homeDashboardHideToolbar = true;
  116 + if (user.additionalInfo) {
  117 + if (user.additionalInfo.lang) {
  118 + lang = user.additionalInfo.lang;
  119 + }
  120 + homeDashboardId = user.additionalInfo.homeDashboardId;
  121 + if (isDefinedAndNotNull(user.additionalInfo.homeDashboardHideToolbar)) {
  122 + homeDashboardHideToolbar = user.additionalInfo.homeDashboardHideToolbar;
  123 + }
  124 + }
  125 + if (!lang) {
112 126 lang = this.translate.currentLang;
113 127 }
114 128 this.profile.get('language').setValue(lang);
  129 + this.profile.get('homeDashboardId').setValue(homeDashboardId);
  130 + this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar);
115 131 }
116 132
117 133 confirmForm(): FormGroup {
... ...
... ... @@ -60,6 +60,21 @@
60 60 <mat-label translate>tenant.description</mat-label>
61 61 <textarea matInput formControlName="description" rows="2"></textarea>
62 62 </mat-form-field>
  63 + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
  64 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  65 + <tb-dashboard-autocomplete
  66 + fxFlex
  67 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  68 + formControlName="homeDashboardId"
  69 + [dashboardsScope]="'tenant'"
  70 + [tenantId]="entity?.id.id"
  71 + [selectFirstDashboard]="false"
  72 + ></tb-dashboard-autocomplete>
  73 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  74 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  75 + </mat-checkbox>
  76 + </section>
  77 + </section>
63 78 </div>
64 79 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
65 80 </fieldset>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + .tb-default-dashboard {
  20 + tb-dashboard-autocomplete {
  21 + @media #{$mat-gt-sm} {
  22 + padding-right: 12px;
  23 + }
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding-bottom: 12px;
  27 + }
  28 + }
  29 + mat-checkbox {
  30 + @media #{$mat-gt-sm} {
  31 + margin-top: 16px;
  32 + }
  33 + }
  34 + }
  35 +}
... ...
... ... @@ -23,11 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
23 23 import { TranslateService } from '@ngx-translate/core';
24 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 28 @Component({
28 29 selector: 'tb-tenant',
29 30 templateUrl: './tenant.component.html',
30   - styleUrls: []
  31 + styleUrls: ['./tenant.component.scss']
31 32 })
32 33 export class TenantComponent extends ContactBasedComponent<TenantInfo> {
33 34
... ... @@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
54 55 tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]],
55 56 additionalInfo: this.fb.group(
56 57 {
57   - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
  58 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  59 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  60 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  61 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
58 62 }
59 63 )
60 64 }
... ... @@ -65,6 +69,11 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
65 69 this.entityForm.patchValue({title: entity.title});
66 70 this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId});
67 71 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  72 + this.entityForm.patchValue({additionalInfo:
  73 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  74 + this.entityForm.patchValue({additionalInfo:
  75 + {homeDashboardHideToolbar: entity.additionalInfo &&
  76 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
68 77 }
69 78
70 79 updateFormState() {
... ...
... ... @@ -95,6 +95,20 @@
95 95 {{ 'user.always-fullscreen' | translate }}
96 96 </mat-checkbox>
97 97 </section>
  98 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  99 + <tb-dashboard-autocomplete
  100 + fxFlex
  101 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  102 + formControlName="homeDashboardId"
  103 + [dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
  104 + [tenantId]="entity?.tenantId?.id"
  105 + [customerId]="entity?.customerId?.id"
  106 + [selectFirstDashboard]="false"
  107 + ></tb-dashboard-autocomplete>
  108 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  109 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  110 + </mat-checkbox>
  111 + </section>
98 112 </section>
99 113 </div>
100 114 </fieldset>
... ...
... ... @@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model';
23 23 import { selectAuth } from '@core/auth/auth.selectors';
24 24 import { map } from 'rxjs/operators';
25 25 import { Authority } from '@shared/models/authority.enum';
26   -import { isUndefined } from '@core/utils';
  26 +import { isDefinedAndNotNull, isUndefined } from '@core/utils';
27 27 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
28 28
29 29 @Component({
... ... @@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent<User> {
74 74 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
75 75 defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
76 76 defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
  77 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  78 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  79 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
77 80 }
78 81 )
79 82 }
... ... @@ -89,6 +92,11 @@ export class UserComponent extends EntityComponent<User> {
89 92 {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
90 93 this.entityForm.patchValue({additionalInfo:
91 94 {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}});
  95 + this.entityForm.patchValue({additionalInfo:
  96 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  97 + this.entityForm.patchValue({additionalInfo:
  98 + {homeDashboardHideToolbar: entity.additionalInfo &&
  99 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
92 100 }
93 101
94 102 }
... ...
... ... @@ -35,6 +35,7 @@
35 35 [isEditActionEnabled]="true"
36 36 [isExportActionEnabled]="true"
37 37 [isRemoveActionEnabled]="!isReadOnly"
  38 + [disableWidgetInteraction]="true"
38 39 [callbacks]="dashboardCallbacks"></tb-dashboard>
39 40 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
40 41 </tb-footer-fab-buttons>
... ...
... ... @@ -14,7 +14,17 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  17 +import {
  18 + Component,
  19 + forwardRef,
  20 + Inject,
  21 + Injector,
  22 + Input,
  23 + OnInit,
  24 + StaticProvider,
  25 + ViewChild,
  26 + ViewContainerRef
  27 +} from '@angular/core';
18 28 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 29 import { Observable, of } from 'rxjs';
20 30 import { PageLink } from '@shared/models/page/page-link';
... ... @@ -32,7 +42,7 @@ import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef
32 42 import { BreakpointObserver } from '@angular/cdk/layout';
33 43 import { DOCUMENT } from '@angular/common';
34 44 import { WINDOW } from '@core/services/window.service';
35   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  45 +import { ComponentPortal } from '@angular/cdk/portal';
36 46 import {
37 47 DASHBOARD_SELECT_PANEL_DATA,
38 48 DashboardSelectPanelComponent,
... ... @@ -186,12 +196,12 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
186 196 overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
187 197 }
188 198
189   - private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector {
190   - const injectionTokens = new WeakMap<any, any>([
191   - [DASHBOARD_SELECT_PANEL_DATA, data],
192   - [OverlayRef, overlayRef]
193   - ]);
194   - return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  199 + private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): Injector {
  200 + const providers: StaticProvider[] = [
  201 + {provide: DASHBOARD_SELECT_PANEL_DATA, useValue: data},
  202 + {provide: OverlayRef, useValue: overlayRef}
  203 + ];
  204 + return Injector.create({parent: this.viewContainerRef.injector, providers});
195 205 }
196 206
197 207 private updateView() {
... ...
... ... @@ -28,12 +28,17 @@ class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFiel
28 28 );
29 29 });
30 30
  31 + let row = false;
  32 + if (this.props.form.direction === 'row') {
  33 + row = true;
  34 + }
  35 +
31 36 return (
32 37 <FormControl component='fieldset'
33 38 className={this.props.form.htmlClass}
34 39 disabled={this.props.form.readonly}>
35 40 <FormLabel component='legend'>{this.props.form.title}</FormLabel>
36   - <RadioGroup name={this.props.form.title} value={this.props.value} onChange={(e) => {
  41 + <RadioGroup row={row} name={this.props.form.title} value={this.props.value} onChange={(e) => {
37 42 this.props.onChangeValidate(e);
38 43 }}>
39 44 {items}
... ...
... ... @@ -22,6 +22,7 @@ import {
22 22 KeyLabelItem
23 23 } from '@shared/components/json-form/react/json-form.models';
24 24 import { Mode } from 'rc-select/lib/interface';
  25 +import { deepClone } from '@core/utils';
25 26
26 27 interface ThingsboardRcSelectState extends JsonFormFieldState {
27 28 currentValue: KeyLabelItem | KeyLabelItem[];
... ... @@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
151 152 labelClass += ' tb-focused';
152 153 }
153 154 let mode: Mode;
154   - if (this.props.form.tags) {
155   - mode = 'tags';
156   - } else if (this.props.form.multiple) {
157   - mode = 'multiple';
  155 + let value = this.state.currentValue;
  156 + if (this.props.form.tags || this.props.form.multiple) {
  157 + value = deepClone(value);
  158 + if (this.props.form.tags) {
  159 + mode = 'tags';
  160 + } else if (this.props.form.multiple) {
  161 + mode = 'multiple';
  162 + }
158 163 }
159 164
160 165 const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}};
... ... @@ -176,12 +181,13 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
176 181 maxTagTextLength={this.props.form.maxTagTextLength}
177 182 disabled={this.props.form.readonly}
178 183 optionLabelProp='children'
179   - value={this.state.currentValue}
  184 + value={value}
180 185 labelInValue={true}
181 186 onSelect={this.onSelect}
182 187 onDeselect={this.onDeselect}
183 188 onFocus={this.onFocus}
184 189 onBlur={this.onBlur}
  190 + placeholder={this.props.form.placeholder}
185 191 style={this.props.form.style || {width: '100%'}}>
186 192 {options}
187 193 </Select>
... ...
... ... @@ -14,7 +14,18 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  17 +import {
  18 + Component,
  19 + forwardRef,
  20 + Inject,
  21 + Injector,
  22 + Input,
  23 + OnDestroy,
  24 + OnInit,
  25 + StaticProvider,
  26 + ViewChild,
  27 + ViewContainerRef
  28 +} from '@angular/core';
18 29 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 30 import { TranslateService } from '@ngx-translate/core';
20 31 import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
... ... @@ -32,7 +43,7 @@ import {
32 43 TimewindowPanelComponent,
33 44 TimewindowPanelData
34 45 } from '@shared/components/time/timewindow-panel.component';
35   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  46 +import { ComponentPortal } from '@angular/cdk/portal';
36 47 import { MediaBreakpoints } from '@shared/models/constants';
37 48 import { BreakpointObserver } from '@angular/cdk/layout';
38 49 import { WINDOW } from '@core/services/window.service';
... ... @@ -229,12 +240,12 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
229 240 });
230 241 }
231 242
232   - private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector {
233   - const injectionTokens = new WeakMap<any, any>([
234   - [TIMEWINDOW_PANEL_DATA, data],
235   - [OverlayRef, overlayRef]
236   - ]);
237   - return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  243 + private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): Injector {
  244 + const providers: StaticProvider[] = [
  245 + {provide: TIMEWINDOW_PANEL_DATA, useValue: data},
  246 + {provide: OverlayRef, useValue: overlayRef}
  247 + ];
  248 + return Injector.create({parent: this.viewContainerRef.injector, providers});
238 249 }
239 250
240 251 registerOnChange(fn: any): void {
... ...
... ... @@ -20,9 +20,11 @@ import {
20 20 Directive,
21 21 ElementRef, HostBinding,
22 22 Inject,
  23 + Injector,
23 24 Input,
24 25 NgZone,
25 26 OnDestroy, Optional,
  27 + StaticProvider,
26 28 ViewChild,
27 29 ViewContainerRef
28 30 } from '@angular/core';
... ... @@ -34,7 +36,6 @@ import { BreakpointObserver } from '@angular/cdk/layout';
34 36 import { MediaBreakpoints } from '@shared/models/constants';
35 37 import { MatButton } from '@angular/material/button';
36 38 import Timeout = NodeJS.Timeout;
37   -import { PortalInjector } from '@angular/cdk/portal';
38 39
39 40 @Directive({
40 41 selector: '[tb-toast]'
... ... @@ -138,10 +139,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
138 139 this.toastComponentRef.destroy();
139 140 }
140 141 };
141   - const injectionTokens = new WeakMap<any, any>([
142   - [MAT_SNACK_BAR_DATA, data]
143   - ]);
144   - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  142 + const providers: StaticProvider[] = [
  143 + {provide: MAT_SNACK_BAR_DATA, useValue: data}
  144 + ];
  145 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
145 146 this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector);
146 147 this.cd.detectChanges();
147 148
... ...
... ... @@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo {
106 106 configuration?: DashboardConfiguration;
107 107 }
108 108
  109 +export interface HomeDashboard extends Dashboard {
  110 + hideDashboardToolbar: boolean;
  111 +}
  112 +
  113 +export interface HomeDashboardInfo {
  114 + dashboardId: DashboardId;
  115 + hideDashboardToolbar: boolean;
  116 +}
  117 +
109 118 export function isPublicDashboard(dashboard: DashboardInfo): boolean {
110 119 if (dashboard && dashboard.assignedCustomers) {
111 120 return dashboard.assignedCustomers
... ...
... ... @@ -54,7 +54,10 @@
54 54 "share-via": "Sdílet přes {{provider}}",
55 55 "continue": "Pokračovat",
56 56 "discard-changes": "Zahodit změny",
57   - "download": "Stáhnout"
  57 + "download": "Stáhnout",
  58 + "next-with-label": "Další: {{label}}",
  59 + "read-more": "Zobrazit více",
  60 + "hide": "Skrýt"
58 61 },
59 62 "aggregation": {
60 63 "aggregation": "Agregace",
... ... @@ -77,6 +80,8 @@
77 80 "test-mail-sent": "Testovací zpráva byla úspěšně odeslána!",
78 81 "base-url": "Základní URL",
79 82 "base-url-required": "Hodnota Základní URL je povinná.",
  83 + "prohibit-different-url": "Zakázat použití názvu hosta z hlaviček požadavku klienta",
  84 + "prohibit-different-url-hint": "Toto nastavení by mělo být povoleno v produkčních prostředích. Pokud je zakázáno, může způsobit bezpečnostní problémy",
80 85 "mail-from": "Email od",
81 86 "mail-from-required": "Hodnota Email od je povinná.",
82 87 "smtp-protocol": "SMTP protokol",
... ... @@ -99,6 +104,33 @@
99 104 "proxy-user": "Uživatel proxy",
100 105 "proxy-password": "Heslo proxy",
101 106 "send-test-mail": "Odeslat testovací zprávu",
  107 + "sms-provider": "Poskytovatel SMS",
  108 + "sms-provider-settings": "Nastavení poskytovatele SMS",
  109 + "sms-provider-type": "Typ poskytovatele SMS",
  110 + "sms-provider-type-required": "Typ poskytovatele SMS je povinný.",
  111 + "sms-provider-type-aws-sns": "Amazon SNS",
  112 + "sms-provider-type-twilio": "Twilio",
  113 + "aws-access-key-id": "AWS Access Key ID",
  114 + "aws-access-key-id-required": "AWS Access Key ID je povinný",
  115 + "aws-secret-access-key": "AWS Secret Access Key",
  116 + "aws-secret-access-key-required": "AWS Secret Access Key je povinný",
  117 + "aws-region": "AWS Region",
  118 + "aws-region-required": "AWS Region je povinný",
  119 + "number-from": "Telefonní číslo odesílatele",
  120 + "number-from-required": "Telefonní číslo Odesílatele je povinné.",
  121 + "number-to": "Telefonní číslo příjemce",
  122 + "number-to-required": "Telefonní číslo příjemce je povinné.",
  123 + "phone-number-hint": "Telefonní číslo ve formátu E.164, např. +19995550123",
  124 + "phone-number-pattern": "Neplatné telefonní číslo. Mělo by odpovídat formátu E.164, např. +19995550123.",
  125 + "sms-message": "SMS zpráva",
  126 + "sms-message-required": "SMS zpráva je povinná.",
  127 + "sms-message-max-length": "SMS zpráva nemůže být delší než 1600 znaků",
  128 + "twilio-account-sid": "Twilio Account SID",
  129 + "twilio-account-sid-required": "Twilio Account SID je povinné",
  130 + "twilio-account-token": "Twilio Account Token",
  131 + "twilio-account-token-required": "Twilio Account Token je povinný",
  132 + "send-test-sms": "Odeslat testovací SMS",
  133 + "test-sms-sent": "Testovací SMS úspěšně odeslána!",
102 134 "security-settings": "Bezpečnostní nastavení",
103 135 "password-policy": "Politika hesel",
104 136 "minimum-password-length": "Minimální délka hesla",
... ... @@ -119,8 +151,74 @@
119 151 "general-policy": "Obecná politika",
120 152 "max-failed-login-attempts": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu",
121 153 "minimum-max-failed-login-attempts-range": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu nemůže být záporný",
122   - "user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email"
123   - },
  154 + "user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email",
  155 + "domain-name": "Doménové jméno",
  156 + "domain-name-unique": "Doménové jméno a protokol musí být unikátní.",
  157 + "error-verification-url": "Doménové jméno by nemělo obsahovat symbol '/' ani ':'. Příklad: thingsboard.io",
  158 + "oauth2": {
  159 + "access-token-uri": "URI přístupového tokenu",
  160 + "access-token-uri-required": "URI přístupového tokenu je povinné.",
  161 + "activate-user": "Aktivovat uživatele",
  162 + "add-domain": "Přidat doménu",
  163 + "delete-domain": "Smazat doménu",
  164 + "add-provider": "Přidat poskytovatele",
  165 + "delete-provider": "Smazat poskytovatele",
  166 + "allow-user-creation": "Povolit vytvoření uživatele",
  167 + "always-fullscreen": "Vždy v režimu celé obrazovky",
  168 + "authorization-uri": "Autorizační URI",
  169 + "authorization-uri-required": "Autorizační URI je povinné.",
  170 + "client-authentication-method": "Metoda autentizace klienta",
  171 + "client-id": "ID klienta",
  172 + "client-id-required": "ID klienta je povinné.",
  173 + "client-secret": "Heslo klienta",
  174 + "client-secret-required": "Heslo klienta je povinné.",
  175 + "custom-setting": "Vlastní nastavení",
  176 + "customer-name-pattern": "Vzor názvu zákazníka",
  177 + "default-dashboard-name": "Název defaultního dashboardu",
  178 + "delete-domain-text": "Budťe opatrní, protože po potvrzení nebudou doména ani žádná data poskytovatele dostupné.",
  179 + "delete-domain-title": "Jste si jisti, že chcete smazat nastavení domény '{{domainName}}'?",
  180 + "delete-registration-text": "Buďte opatrní, protože po potvrzení nebudou data poskytovatele dostupná.",
  181 + "delete-registration-title": "Jste si jisti, že chcete smazat poskytovatele '{{name}}'?",
  182 + "email-attribute-key": "Atribut klíče email",
  183 + "email-attribute-key-required": "Atribut klíče email je povinný.",
  184 + "first-name-attribute-key": "Atribut klíče jméno",
  185 + "general": "Obecné",
  186 + "jwk-set-uri": "JSON Web Key URI",
  187 + "last-name-attribute-key": "Atribut klíče příjmení",
  188 + "login-button-icon": "Ikona tlačítka přihlášení",
  189 + "login-button-label": "Označení poskytovatele",
  190 + "login-button-label-placeholder": "Přihlásit se přes $(Provider label)",
  191 + "login-button-label-required": "Označení je povinné.",
  192 + "login-provider": "Poskytovatel přihlášení",
  193 + "mapper": "Mapper",
  194 + "new-domain": "Nová doména",
  195 + "oauth2": "OAuth2",
  196 + "redirect-uri-template": "Šablona URI přesměrování",
  197 + "copy-redirect-uri": "Zkopírovat URI přesměrování",
  198 + "registration-id": "ID registrace",
  199 + "registration-id-required": "ID registrace je povinné.",
  200 + "registration-id-unique": "Id registrace musí být v systému unikátní.",
  201 + "scope": "Rozsah",
  202 + "scope-required": "Rozsah je povinný.",
  203 + "tenant-name-pattern": "Vzor názvu tenanta",
  204 + "tenant-name-pattern-required": "Vzor názvu tenanta je povinný.",
  205 + "tenant-name-strategy": "Strategie názvu tenanta",
  206 + "type": "Typ mapperu",
  207 + "uri-pattern-error": "Neplatný formát URI.",
  208 + "url": "URL",
  209 + "url-pattern": "Neplatný formát URL.",
  210 + "url-required": "URL je povinná.",
  211 + "user-info-uri": "User info URI",
  212 + "user-info-uri-required": "User info URI je povinné.",
  213 + "user-name-attribute-name": "Atribut klíče název uživatele",
  214 + "user-name-attribute-name-required": "Atribut klíče název uživatele je povinný",
  215 + "protocol": "Protokol",
  216 + "domain-schema-http": "HTTP",
  217 + "domain-schema-https": "HTTPS",
  218 + "domain-schema-mixed": "HTTP+HTTPS",
  219 + "enable": "Povolit nastavení OAuth2"
  220 + }
  221 + },
124 222 "alarm": {
125 223 "alarm": "Alarm",
126 224 "alarms": "Alarmy",
... ... @@ -128,6 +226,8 @@
128 226 "no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.",
129 227 "alarm-required": "Alarm je povinný",
130 228 "alarm-status": "Stav alarmu",
  229 + "alarm-status-list": "Seznam stavů alarmu",
  230 + "any-status": "Všechny stavy",
131 231 "search-status": {
132 232 "ANY": "Všechny",
133 233 "ACTIVE": "Aktivní",
... ... @@ -154,6 +254,8 @@
154 254 "end-time": "Datum ukončení",
155 255 "ack-time": "Datum přijetí",
156 256 "clear-time": "Datum vyřešení",
  257 + "alarm-severity-list": "Seznam závažností alarmu",
  258 + "any-severity": "Všechny závažnosti",
157 259 "severity-critical": "Kritická",
158 260 "severity-major": "Vysoká",
159 261 "severity-minor": "Nízká",
... ... @@ -176,12 +278,16 @@
176 278 "clear-alarm-title": "Odstranit alarm",
177 279 "clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?",
178 280 "alarm-status-filter": "Filtr stavu alarmu",
  281 + "alarm-filter": "Filtr alarmu",
179 282 "max-count-load": "Maximální počet nahraných alarmů (0 - neomezeně)",
180 283 "max-count-load-required": "Maximální počet nahraných alarmů je povinný.",
181 284 "max-count-load-error-min": "Minimální hodnota je 0.",
182 285 "fetch-size": "Velikost dávky",
183 286 "fetch-size-required": "Velikost dávky je povinná.",
184   - "fetch-size-error-min": "Minimální hodnota je 10."
  287 + "fetch-size-error-min": "Minimální hodnota je 10.",
  288 + "alarm-type-list": "Seznam typů alarmu",
  289 + "any-type": "Všechny typy",
  290 + "search-propagated-alarms": "Vyhledat zpropagované alarmy"
185 291 },
186 292 "alias": {
187 293 "add": "Přidat alias",
... ... @@ -211,6 +317,7 @@
211 317 "filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
212 318 "filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity",
213 319 "filter-type-entity-view-search-query-description": "Entitní pohledy typů {{entityViewTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
  320 + "filter-type-apiUsageState": "Stav využití Api",
214 321 "entity-filter": "Filtr entity",
215 322 "resolve-multiple": "Použít jako více entit",
216 323 "filter-type": "Typ filtru",
... ... @@ -325,6 +432,59 @@
325 432 "no-attributes-text": "Žádné atributy nebyly nalezeny",
326 433 "no-telemetry-text": "Žádná telemetrie nebyla nalezena"
327 434 },
  435 + "api-usage": {
  436 + "api-usage": "Využití Api",
  437 + "data-points": "Datové body",
  438 + "data-points-storage-days": "Dny uložení datových bodů",
  439 + "email": "Email",
  440 + "email-messages": "Emailové zprávy",
  441 + "email-messages-daily-activity": "Denní aktivita emailových zpráv",
  442 + "email-messages-hourly-activity": "Hodinová aktivita emailových zpráv",
  443 + "email-messages-monthly-activity": "Měsíční aktivita emailových zpráv",
  444 + "exceptions": "Výjimky",
  445 + "executions": "Zpracování",
  446 + "javascript": "JavaScript",
  447 + "javascript-executions": "JavaScript výjimky",
  448 + "javascript-functions": "JavaScript funkce",
  449 + "javascript-functions-daily-activity": "Denní aktivita JavaScript funkcí",
  450 + "javascript-functions-hourly-activity": "Hodinová aktivita JavaScript funkcí",
  451 + "javascript-functions-monthly-activity": "Měsíční aktivita JavaScript funkcí",
  452 + "latest-error": "Poslední chyba",
  453 + "messages": "Zprávy",
  454 + "permanent-failures": "${entityName} permanentní chyby",
  455 + "permanent-timeouts": "${entityName} permanentní timeouty",
  456 + "processing-failures": "${entityName} chyby zpracování",
  457 + "processing-failures-and-timeouts": "Chyby a timeouty zpracování",
  458 + "processing-timeouts": "${entityName} timeouty zpracování",
  459 + "queue-stats": "Statistiky fronty",
  460 + "rule-chain": "Řetěz pravidel",
  461 + "rule-engine": "Engine pro zpracování pravidel",
  462 + "rule-engine-daily-activity": "Denní aktivita enginu pro zpracování pravidel",
  463 + "rule-engine-executions": "Zpracování Enginu pro zpracování pravidel",
  464 + "rule-engine-hourly-activity": "Hodinová aktivita enginu pro zpracování pravidel",
  465 + "rule-engine-monthly-activity": "Měsíční aktivita enginu pro zpracování pravidel",
  466 + "rule-engine-statistics": "Statistiky enginu pro zpracování pravidel",
  467 + "rule-node": "Uzel pravidla",
  468 + "sms": "SMS",
  469 + "sms-messages": "SMS zprávy",
  470 + "sms-messages-daily-activity": "Denní aktivita SMS zpráv",
  471 + "sms-messages-hourly-activity": "Hodinová aktivita SMS zpráv",
  472 + "sms-messages-monthly-activity": "Měsíční aktivita SMS zpráv",
  473 + "successful": "${entityName} úspěšnost",
  474 + "telemetry": "Telemetrie",
  475 + "telemetry-persistence": "Uložení telemetrie",
  476 + "telemetry-persistence-daily-activity": "Denní aktivita uložení telemetrie",
  477 + "telemetry-persistence-hourly-activity": "Hodinová aktivita uložení telemetrie",
  478 + "telemetry-persistence-monthly-activity": "Měsíční aktivita uložení telemetrie",
  479 + "transport": "Přenos",
  480 + "transport-daily-activity": "Denní aktivita přenosu",
  481 + "transport-data-points": "Datové body přenosu",
  482 + "transport-hourly-activity": "Hodinová aktivita přenosu",
  483 + "transport-messages": "Zprávy přenosu",
  484 + "transport-monthly-activity": "Měsíční aktivita přenosu",
  485 + "view-details": "Zobrazit detail",
  486 + "view-statistics": "Zobrazit statistiky"
  487 + },
328 488 "audit-log": {
329 489 "audit": "Audit",
330 490 "audit-logs": "Záznamy auditu",
... ... @@ -363,7 +523,13 @@
363 523 "action-data": "Data akce",
364 524 "failure-details": "Detail chyby",
365 525 "search": "Prohledat záznamy auditu",
366   - "clear-search": "Vymazat vyhledávání"
  526 + "clear-search": "Vymazat vyhledávání",
  527 + "type-assigned-from-tenant": "Odebráno tenantovi",
  528 + "type-assigned-to-tenant": "Přiřazeno tenantovi",
  529 + "type-provision-success": "Zřízení zařízení",
  530 + "type-provision-failure": "Selhání zřízení zařízení",
  531 + "type-timeseries-updated": "Aktualizace telemetrie",
  532 + "type-timeseries-deleted": "Smazání telemetrie"
367 533 },
368 534 "confirm-on-exit": {
369 535 "message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?",
... ... @@ -549,6 +715,7 @@
549 715 "title-color": "Barva názvu",
550 716 "display-dashboards-selection": "Zobrazit výběr dashboardů",
551 717 "display-entities-selection": "Zobrazit výběr entit",
  718 + "display-filters": "Zobrazit filtry",
552 719 "display-dashboard-timewindow": "Zobrazit časové okno",
553 720 "display-dashboard-export": "Zobrazit export",
554 721 "import": "Importovat dashboard",
... ... @@ -615,6 +782,7 @@
615 782 "alarm": "Pole alarmu",
616 783 "timeseries-required": "Časové řady entity jsou povinné.",
617 784 "timeseries-or-attributes-required": "Časové řady / atributy entity jsou povinné.",
  785 + "alarm-fields-timeseries-or-attributes-required": "Pole alarmu nebo časové řady / atributy jsou povinné.",
618 786 "maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }",
619 787 "alarm-fields-required": "Pole alarmu jsou povinná.",
620 788 "function-types": "Typy funkcí",
... ... @@ -706,6 +874,12 @@
706 874 "access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.",
707 875 "rsa-key": "RSA veřejný klíč",
708 876 "rsa-key-required": "RSA veřejný klíč je povinný.",
  877 + "client-id": "ID klienta",
  878 + "client-id-pattern": "Obsahuje neplatné znaky.",
  879 + "user-name": "Název uživatele",
  880 + "user-name-required": "Název uživatele je povinný.",
  881 + "client-id-or-user-name-necessary": "ID klienta a/nebo název uživatele jsou povinné",
  882 + "password": "Heslo",
709 883 "secret": "Heslo",
710 884 "secret-required": "Heslo je povinné.",
711 885 "device-type": "Typ zařízení",
... ... @@ -724,19 +898,183 @@
724 898 "details": "Detail",
725 899 "copyId": "Kopírovat Id zařízení",
726 900 "copyAccessToken": "Kopírovat přístupový token",
  901 + "copy-mqtt-authentication": "Kopírovat přístupové údaje MQTT",
727 902 "idCopiedMessage": "Id zařízení bylo zkopírováno do schránky",
728 903 "accessTokenCopiedMessage": "Přístupový token zařízení byl zkopírován do schránky",
  904 + "mqtt-authentication-copied-message": "MQTT autentizace zařízení byla zkopírována do schránky",
729 905 "assignedToCustomer": "Přiřazeno zákazníkovi",
730 906 "unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení",
731 907 "unable-delete-device-alias-text": "Alias zařízení '{{deviceAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}",
732 908 "is-gateway": "Je bránou",
  909 + "overwrite-activity-time": "Přepsat čas aktivity připojeného zařízení",
733 910 "public": "Veřejné",
734 911 "device-public": "Zařízení je veřejné",
735 912 "select-device": "Vybrat zařízení",
736 913 "import": "Importovat zařízení",
737 914 "device-file": "Soubor zařízení",
738 915 "search": "Vyhledat zařízení",
739   - "selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }"
  916 + "selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }",
  917 + "device-configuration": "Konfigurace zařízení",
  918 + "transport-configuration": "Konfigurace přenosu",
  919 + "wizard": {
  920 + "device-wizard": "Průvodce zařízením",
  921 + "device-details": "Detail zařízení",
  922 + "new-device-profile": "Vytvořit nový profil zařízení",
  923 + "existing-device-profile": "Vybrat existující profil zařízení",
  924 + "specific-configuration": "Specifická konfigurace",
  925 + "customer-to-assign-device": "Přiřadit zařízení zákazníkovi",
  926 + "add-credential": "Přidat přístupový údaj"
  927 + }
  928 + },
  929 + "device-profile": {
  930 + "device-profile": "Profil zařízení",
  931 + "device-profiles": "Profily zařízení",
  932 + "all-device-profiles": "Všechny",
  933 + "add": "Přidat profil zařízení",
  934 + "edit": "Editovat profil zařízení",
  935 + "device-profile-details": "Detail profilu zařízení",
  936 + "no-device-profiles-text": "Žádné profily zařízení nebyly nalezeny",
  937 + "search": "Vyhledat profily zařízení",
  938 + "selected-device-profiles": "Vybráno { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }",
  939 + "no-device-profiles-matching": "Žádný profil zařízení odpovídající '{{entity}}' nebyl nalezen.",
  940 + "device-profile-required": "Profil zařízení je povinný",
  941 + "idCopiedMessage": "Id profilu zařízení bylo zkopírováno do schránky",
  942 + "set-default": "Učinit profil zařízení defaultním",
  943 + "delete": "Smazat profil zařízení",
  944 + "copyId": "Kopírovat Id profilu zařízení",
  945 + "new-device-profile-name": "Název profilu zařízení",
  946 + "new-device-profile-name-required": "Název profilu zařízení je povinný.",
  947 + "name": "Název",
  948 + "name-required": "Název je povinný.",
  949 + "type": "Typ profilu",
  950 + "type-required": "Typ profilu je povinný.",
  951 + "type-default": "Defaultní",
  952 + "transport-type": "Typ přenosu",
  953 + "transport-type-required": "Typ přenosu je povinný.",
  954 + "transport-type-default": "Defaultní",
  955 + "transport-type-default-hint": "Podporuje základní MQTT, HTTP and CoAP přenos",
  956 + "transport-type-mqtt": "MQTT",
  957 + "transport-type-mqtt-hint": "Umožňuje pokročilé nastavení MQTT přenosu",
  958 + "transport-type-lwm2m": "LWM2M",
  959 + "transport-type-lwm2m-hint": "Typ transportu LWM2M",
  960 + "description": "Popis",
  961 + "default": "Defaultní",
  962 + "profile-configuration": "Konfigurace profilu",
  963 + "transport-configuration": "Konfigurace přenosu",
  964 + "default-rule-chain": "Defaultní řetěz pravidel",
  965 + "select-queue-hint": "Vyberte z rozbalovacího seznamu nebo přidejte vlastní název.",
  966 + "delete-device-profile-title": "Jste si jisti, že chcete smazat profil zařízení '{{deviceProfileName}}'?",
  967 + "delete-device-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil zařízení ani žádná související data obnovit.",
  968 + "delete-device-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }?",
  969 + "delete-device-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily zařízení odstraněny a žádná související data nebude možné obnovit.",
  970 + "set-default-device-profile-title": "Jste si jisti, že chcete profil zařízení '{{deviceProfileName}}' učinit defaultním?",
  971 + "set-default-device-profile-text": "Po potvrzení bude profil zařízení označen jako defaultní a bude použit pro nová zařízení bez specifikovaného profilu.",
  972 + "no-device-profiles-found": "Žádné profily zařízení nebyly nalezeny.",
  973 + "create-new-device-profile": "Vytvořit nový!",
  974 + "mqtt-device-topic-filters": "Filtry MQTT fronty zařízení",
  975 + "mqtt-device-topic-filters-unique": "Filtry MQTT fronty zařízení musí být unikátní.",
  976 + "mqtt-device-payload-type": "MQTT zpráva zařízení",
  977 + "mqtt-device-payload-type-json": "JSON",
  978 + "mqtt-device-payload-type-proto": "Protobuf",
  979 + "mqtt-payload-type-required": "Typ zprávy je povinný.",
  980 + "support-level-wildcards": "Jsou podporovány jednoúrovňové <code>[+]</code> a víceúrovňové <code>[#]</code> zástupné znaky.",
  981 + "telemetry-topic-filter": "Filtr fronty telemetrie",
  982 + "telemetry-topic-filter-required": "Filtr fronty telemetrie je povinný.",
  983 + "attributes-topic-filter": "Filtr atributů fronty",
  984 + "attributes-topic-filter-required": "Filtr atributů fronty je povinný.",
  985 + "telemetry-proto-schema": "Proto schéma telemetrie",
  986 + "telemetry-proto-schema-required": "Proto schéma telemetrie je povinné.",
  987 + "attributes-proto-schema": "Atributy proto schémata",
  988 + "attributes-proto-schema-required": "Atributy proto schémata jsou povinné.",
  989 + "rpc-response-topic-filter": "Filtr fronty RPC odpovědi",
  990 + "rpc-response-topic-filter-required": "Filtr fronty RPC odpovědi je povinný.",
  991 + "not-valid-pattern-topic-filter": "Neplatný vzor filtru fronty",
  992 + "not-valid-single-character": "Neplatné použití jednoúrovňového zástupného znaku",
  993 + "not-valid-multi-character": "Neplatné použití víceúrovňového zástupného znaku",
  994 + "single-level-wildcards-hint": "<code>[+]</code> je vhodný pro jakoukoli úroveň filtru fronty. Př.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
  995 + "multi-level-wildcards-hint": "<code>[#]</code> může nahradit filtr fronty a může se jednat o poslední symbol fronty. Př.: <b>#</b> or <b>v1/devices/me/#</b>.",
  996 + "alarm-rules": "Pravidla alarmu",
  997 + "alarm-rules-with-count": "Pravidla alarmu ({{count}})",
  998 + "no-alarm-rules": "Žádná pravidla alarmu nejsou konfigurována",
  999 + "add-alarm-rule": "Přidat pravidlo alarmu",
  1000 + "edit-alarm-rule": "Editovat pravidlo alarmu",
  1001 + "alarm-type": "Typ alarmu",
  1002 + "alarm-type-required": "Typ alarmu je povinný.",
  1003 + "alarm-type-unique": "Typ alarmu musí být v rámci pravidel alarmu profilu zařízení unikátní.",
  1004 + "create-alarm-pattern": "Vytvořit <b>{{alarmType}}</b> alarm",
  1005 + "create-alarm-rules": "Vytvořit pravidla alarmu",
  1006 + "no-create-alarm-rules": "Nejsou konfigurovány žádné podmínky vytvoření",
  1007 + "add-create-alarm-rule-prompt": "Přidejte prosím pravidlo vytvoření alarmu",
  1008 + "clear-alarm-rule": "Pravidlo zrušení alarmu",
  1009 + "no-clear-alarm-rule": "Není konfigurována žádná podmínka zrušení",
  1010 + "add-create-alarm-rule": "Přidat podmínku vytvoření",
  1011 + "add-clear-alarm-rule": "Přidat podmínku zrušení",
  1012 + "select-alarm-severity": "Vybrat závažnost alarmu",
  1013 + "alarm-severity-required": "Závažnost alarmu je povinná.",
  1014 + "condition-duration": "Doba trvání podmínky",
  1015 + "condition-duration-value": "Hodnota doby trvání",
  1016 + "condition-duration-time-unit": "Jednotka času",
  1017 + "condition-duration-value-range": "Hodnota doby trvání musí být v rozsahu od 1 do 2147483647.",
  1018 + "condition-duration-value-pattern": "Doba trvání musí být celé číslo.",
  1019 + "condition-duration-value-required": "Doba trvání je povinná.",
  1020 + "condition-duration-time-unit-required": "Jednotka času je povinná.",
  1021 + "advanced-settings": "Pokročilá nastavení",
  1022 + "alarm-rule-details": "Detail",
  1023 + "add-alarm-rule-details": "Přidat detail",
  1024 + "propagate-alarm": "Propagovat alarm",
  1025 + "alarm-rule-relation-types-list": "Typy vztahů ke zpropagování",
  1026 + "alarm-rule-relation-types-list-hint": "Pokud nejsou vybrány žádné typy vztahů, alarmy budou propagovány bez filtru typu vztahu.",
  1027 + "alarm-details": "Detail alarmu",
  1028 + "alarm-rule-condition": "Podmínka pravidla alarmu",
  1029 + "enter-alarm-rule-condition-prompt": "Přidejte prosím podmínku pravidla alarmu",
  1030 + "edit-alarm-rule-condition": "Editovat podmínku pravidla alarmu",
  1031 + "device-provisioning": "Zřízení zařízení",
  1032 + "provision-strategy": "Strategie zřízení",
  1033 + "provision-strategy-required": "Strategie zřízení je povinná.",
  1034 + "provision-strategy-disabled": "Zakázáno",
  1035 + "provision-strategy-created-new": "Povolit vytváření nových zařízení",
  1036 + "provision-strategy-check-pre-provisioned": "Zkontrolovat předvytvořená zařízení",
  1037 + "provision-device-key": "Klíč pro zřízení zařízení",
  1038 + "provision-device-key-required": "Klíč pro zřízení zařízení je povinný.",
  1039 + "copy-provision-key": "Kopírovat klíč pro zřízení",
  1040 + "provision-key-copied-message": "Klíč pro zřízení byl zkopírován do schránky",
  1041 + "provision-device-secret": "Heslo pro zřízení zařízení",
  1042 + "provision-device-secret-required": "Heslo pro zřízení zařízení je povinné.",
  1043 + "copy-provision-secret": "Kopírovat heslo pro zřízení",
  1044 + "provision-secret-copied-message": "Heslo pro zřízení zařízení bylo zkopírováno do schránky",
  1045 + "condition": "Podmínka",
  1046 + "condition-type": "Typ podmínky",
  1047 + "condition-type-simple": "Jednoduchá",
  1048 + "condition-type-duration": "Doba trvání",
  1049 + "condition-during": "V průběhu {{during}}",
  1050 + "condition-type-repeating": "Opakování",
  1051 + "condition-type-required": "Typ podmínky je povinný.",
  1052 + "condition-repeating-value": "Počet událostí",
  1053 + "condition-repeating-value-range": "Počet událostí musí být v rozsahu od 1 do 2147483647.",
  1054 + "condition-repeating-value-pattern": "Počet událostí musí být celé číslo.",
  1055 + "condition-repeating-value-required": "Počet událostí je povinný.",
  1056 + "condition-repeat-times": "Opakování { count, plural, 1 {1 krát} other {# krát} }",
  1057 + "schedule-type": "Typ plánovače",
  1058 + "schedule-type-required": "Typ plánovače je povinný.",
  1059 + "schedule": "Časový plán",
  1060 + "edit-schedule": "Editovat časový plán alarmu",
  1061 + "schedule-any-time": "Aktivní neustále",
  1062 + "schedule-specific-time": "Aktivní v konkrétním čase",
  1063 + "schedule-custom": "Vlastní",
  1064 + "schedule-day": {
  1065 + "monday": "Pondělí",
  1066 + "tuesday": "Úterý",
  1067 + "wednesday": "Středa",
  1068 + "thursday": "Čtvrtek",
  1069 + "friday": "Pátek",
  1070 + "saturday": "Sobota",
  1071 + "sunday": "Neděle"
  1072 + },
  1073 + "schedule-days": "Dny",
  1074 + "schedule-time": "Čas",
  1075 + "schedule-time-from": "Od",
  1076 + "schedule-time-to": "Do",
  1077 + "schedule-days-of-week-required": "Musí být vybrán minimálně jeden den v týdnu."
740 1078 },
741 1079 "dialog": {
742 1080 "close": "Zavřít dialog"
... ... @@ -757,7 +1095,7 @@
757 1095 "entity-alias": "Alias entity",
758 1096 "unable-delete-entity-alias-title": "Alias entity nebylo možné smazat",
759 1097 "unable-delete-entity-alias-text": "Alias entity '{{entityAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}",
760   - "duplicate-alias-error": "Nalezen dupliticní alias '{{alias}}'.<br>Aliasy entit musí být v rámci dashboardu unikátní.",
  1098 + "duplicate-alias-error": "Nalezen duplicitní alias '{{alias}}'.<br>Aliasy entit musí být v rámci dashboardu unikátní.",
761 1099 "missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.",
762 1100 "configure-alias": "Konfigurovat '{{alias}}' alias",
763 1101 "alias": "Alias",
... ... @@ -794,6 +1132,10 @@
794 1132 "type-devices": "Zařízení",
795 1133 "list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }",
796 1134 "device-name-starts-with": "Zařízení, jejichž název začíná '{{prefix}}'",
  1135 + "type-device-profile": "Profil zařízení",
  1136 + "type-device-profiles": "Profily zařízení",
  1137 + "list-of-device-profiles": "{ count, plural, 1 {Jeden profil zařízení} other {Seznam # profilů zařízení} }",
  1138 + "device-profile-name-starts-with": "Profily zařízení, jejichž název začíná '{{prefix}}'",
797 1139 "type-asset": "Aktivum",
798 1140 "type-assets": "Aktiva",
799 1141 "list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }",
... ... @@ -814,6 +1156,10 @@
814 1156 "type-tenants": "Tenanti",
815 1157 "list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }",
816 1158 "tenant-name-starts-with": "Tenanti, jejichž název začíná '{{prefix}}'",
  1159 + "type-tenant-profile": "Profil tenanta",
  1160 + "type-tenant-profiles": "Profily tenantů",
  1161 + "list-of-tenant-profiles": "{ count, plural, 1 {Jeden profil tenanta} other {Seznam # profilů tenantů} }",
  1162 + "tenant-profile-name-starts-with": "Profily tenantů, jejichž název začíná '{{prefix}}'",
817 1163 "type-customer": "Zákazník",
818 1164 "type-customers": "Zákazníci",
819 1165 "list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }",
... ... @@ -840,6 +1186,8 @@
840 1186 "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'",
841 1187 "type-current-customer": "Stávající zákazník",
842 1188 "type-current-tenant": "Stávající tenant",
  1189 + "type-current-user": "Stávající uživatel",
  1190 + "type-current-user-owner": "Vlastník stávajícího uživatele",
843 1191 "search": "Vyhledat entity",
844 1192 "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno",
845 1193 "entity-name": "Název entity",
... ... @@ -847,7 +1195,8 @@
847 1195 "details": "Detail entity",
848 1196 "no-entities-prompt": "Žádné entity nebyly nalezeny",
849 1197 "no-data": "Nelze zobrazit žádná data",
850   - "columns-to-display": "Zobrazit sloupce"
  1198 + "columns-to-display": "Zobrazit sloupce",
  1199 + "type-api-usage-state": "Stav využití API"
851 1200 },
852 1201 "entity-field": {
853 1202 "created-time": "Datum vytvoření",
... ... @@ -1048,7 +1397,7 @@
1048 1397 "anonymous": "Anonymní",
1049 1398 "basic": "Základní",
1050 1399 "pem": "PEM",
1051   - "ca-cert": "soubor CA certifikátu *",
  1400 + "ca-cert": "Soubor CA certifikátu *",
1052 1401 "private-key": "Soubor privátního klíče *",
1053 1402 "cert": "Soubor certifikátu *",
1054 1403 "no-file": "Žádný soubor nebyl vybrán.",
... ... @@ -1154,6 +1503,93 @@
1154 1503 "file": "Soubor rozšíření",
1155 1504 "invalid-file-error": "Neplatný soubor rozšíření"
1156 1505 },
  1506 + "filter": {
  1507 + "add": "Přidat filtr",
  1508 + "edit": "Editovat filtr",
  1509 + "name": "Název filtru",
  1510 + "name-required": "Název filtru je povinný.",
  1511 + "duplicate-filter": "Filtr s identickým názvem již existuje.",
  1512 + "filters": "Filtry",
  1513 + "unable-delete-filter-title": "Smazat filtr není možné",
  1514 + "unable-delete-filter-text": "Filtr '{{filter}}' není možné smazat, protože je používán následujícím widgetem(y):<br/>{{widgetsList}}",
  1515 + "duplicate-filter-error": "Nalezen duplicitní filtr '{{filter}}'.<br>Filtry musí být v rámci dashboardu unikátní.",
  1516 + "missing-key-filters-error": "U filtru '{{filter}}' chybí klíčové filtry.",
  1517 + "filter": "Filtr",
  1518 + "editable": "Editovatelné",
  1519 + "no-filters-found": "Žádné filtry nebyly nalezeny.",
  1520 + "no-filter-text": "Není specifikován žádný filtr",
  1521 + "add-filter-prompt": "Přidejte prosím filtr",
  1522 + "no-filter-matching": "'{{filter}}' nebyl nalezen.",
  1523 + "create-new-filter": "Vytvořit nový!",
  1524 + "filter-required": "Filtr je povinný.",
  1525 + "operation": {
  1526 + "operation": "Operace",
  1527 + "equal": "je rovno",
  1528 + "not-equal": "není rovno",
  1529 + "starts-with": "začíná na",
  1530 + "ends-with": "končí na",
  1531 + "contains": "obsahuje",
  1532 + "not-contains": "neobsahuje",
  1533 + "greater": "větší než",
  1534 + "less": "menší než",
  1535 + "greater-or-equal": "větší nebo rovno",
  1536 + "less-or-equal": "menší nebo rovno",
  1537 + "and": "a",
  1538 + "or": "nebo"
  1539 + },
  1540 + "ignore-case": "ignorovat velikost písmen",
  1541 + "value": "Hodnota",
  1542 + "remove-filter": "Odebrat filtr",
  1543 + "preview": "Náhled filtru",
  1544 + "no-filters": "Nejsou konfigurovány žádné filtry",
  1545 + "add-filter": "Přidat filtr",
  1546 + "add-complex-filter": "Přidat komplexní filtr",
  1547 + "add-complex": "Přidat komplex",
  1548 + "complex-filter": "Komplexní filtr",
  1549 + "edit-complex-filter": "Editovat komplexní filtr",
  1550 + "edit-filter-user-params": "Editovat filtr predikátu parametrů uživatele",
  1551 + "filter-user-params": "Filtr predikátu parametrů uživatele",
  1552 + "user-parameters": "Parametry uživatele",
  1553 + "display-label": "Zobrazované označení",
  1554 + "autogenerated-label": "Automaticky vygenerovat označení",
  1555 + "order-priority": "Priority pořadí polí",
  1556 + "key-filter": "Klíčový filtr",
  1557 + "key-filters": "Klíčové filtry",
  1558 + "key-name": "Název klíče",
  1559 + "key-name-required": "Název klíče je povinný.",
  1560 + "key-type": {
  1561 + "key-type": "Typ klíče",
  1562 + "attribute": "Atribut",
  1563 + "timeseries": "Časové řady",
  1564 + "entity-field": "Pole entity"
  1565 + },
  1566 + "value-type": {
  1567 + "value-type": "Typ hodnoty",
  1568 + "string": "Řetězec",
  1569 + "numeric": "Číslo",
  1570 + "boolean": "Pravdivostní hodnota",
  1571 + "date-time": "Datum a čas"
  1572 + },
  1573 + "value-type-required": "Typ hodnoty klíče je povinný.",
  1574 + "key-value-type-change-title": "Jste si jisti, že chcete změnit typ klíče hodnoty?",
  1575 + "key-value-type-change-message": "Pokud potvrdíte nový typ hodnoty, všechny zadané klíčové filtry budou odstraněny.",
  1576 + "no-key-filters": "Nejsou konfigurovány žádné klíčové filtry",
  1577 + "add-key-filter": "Přidat klíčový filtr",
  1578 + "remove-key-filter": "Odebrat klíčový filtr",
  1579 + "edit-key-filter": "Editovat klíčový filtr",
  1580 + "date": "Datum",
  1581 + "time": "Čas",
  1582 + "current-tenant": "Stávající tenant",
  1583 + "current-customer": "Stávající zákazník",
  1584 + "current-user": "Stávající uživatel",
  1585 + "current-device": "Stávající zařízení",
  1586 + "default-value": "Defaultní hodnota",
  1587 + "dynamic-source-type": "Dynamický typ zdroje",
  1588 + "no-dynamic-value": "Žádná dynamická hodnota",
  1589 + "source-attribute": "Atribut zdroje",
  1590 + "switch-to-dynamic-value": "Přepnout na dynamickou hodnotu",
  1591 + "switch-to-default-value": "Přepnout na defaultní hodnotu"
  1592 + },
1157 1593 "fullscreen": {
1158 1594 "expand": "Rozšířit do režimu celé obrazovky",
1159 1595 "exit": "Ukončit režim celé obrazovky",
... ... @@ -1286,6 +1722,7 @@
1286 1722 "entity-field": "Pole entity",
1287 1723 "access-token": "Přístupový token",
1288 1724 "isgateway": "Je bránou",
  1725 + "activity-time-from-gateway-device": "Čas aktivity ze zařízení brány",
1289 1726 "description": "Popis"
1290 1727 },
1291 1728 "stepper-text":{
... ... @@ -1329,6 +1766,7 @@
1329 1766 "legend": {
1330 1767 "direction": "Směr legendy",
1331 1768 "position": "Pozice legendy",
  1769 + "sort-legend": "Setřídit datové klíče v legendě",
1332 1770 "show-max": "Zobrazit max hodnotu",
1333 1771 "show-min": "Zobrazit min hodnotu",
1334 1772 "show-avg": "Zobrazit průměrnou hodnotu",
... ... @@ -1525,6 +1963,12 @@
1525 1963 "help": "Nápověda",
1526 1964 "reset-debug-mode": "Resetovat režim ladění na všech uzlech"
1527 1965 },
  1966 + "timezone": {
  1967 + "timezone": "Časová zóna",
  1968 + "select-timezone": "Vyberte časovou zónu",
  1969 + "no-timezones-matching": "žádné časové zóny odpovídající '{{timezone}}' nebyly nalezeny.",
  1970 + "timezone-required": "Časová zóna je povinná."
  1971 + },
1528 1972 "queue": {
1529 1973 "select_name": "Vybrat název fronty",
1530 1974 "name": "Název fronty",
... ... @@ -1563,6 +2007,87 @@
1563 2007 "isolated-tb-core-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta",
1564 2008 "isolated-tb-rule-engine-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta"
1565 2009 },
  2010 + "tenant-profile": {
  2011 + "tenant-profile": "Profil tenanta",
  2012 + "tenant-profiles": "Profily tenantů",
  2013 + "add": "Přidat profil tenanta",
  2014 + "edit": "Editovat profil tenanta",
  2015 + "tenant-profile-details": "Detail profilu tenanta",
  2016 + "no-tenant-profiles-text": "Nebyly nalezeny žádné profily tenantů",
  2017 + "search": "Vyhledat profily tenantů",
  2018 + "selected-tenant-profiles": "Vybráno { count, plural, 1 {1 profilů tenantů} other {# profilů tenantů} }",
  2019 + "no-tenant-profiles-matching": "Žádné profily tenantů odpovídající '{{entity}}' nebyly nalezeny.",
  2020 + "tenant-profile-required": "Profil tenanta je povinný",
  2021 + "idCopiedMessage": "Id profilu tenanta bylo zkopírováno do schránky",
  2022 + "set-default": "Učinit profil tenanta defaultním",
  2023 + "delete": "Smazat profil tenanta",
  2024 + "copyId": "Kopírovat Id profilu tenanta",
  2025 + "name": "Název",
  2026 + "name-required": "Název je povinný.",
  2027 + "data": "Data profilu",
  2028 + "profile-configuration": "Konfigurace profilu",
  2029 + "description": "Popis",
  2030 + "default": "Defaultní",
  2031 + "delete-tenant-profile-title": "Jste si jisti, že chcete smazat profil tenanta '{{tenantProfileName}}'?",
  2032 + "delete-tenant-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil tenanta ani žádná související data obnovit.",
  2033 + "delete-tenant-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil tenanta} other {# profilů tenanta} }?",
  2034 + "delete-tenant-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily tenantů odstraněny a žádná související data nebude možné obnovit.",
  2035 + "set-default-tenant-profile-title": "Jste si jisti, že chcete učinit profil tenanta '{{tenantProfileName}}' defaultním?",
  2036 + "set-default-tenant-profile-text": "Po potvrzení bude profil tenanta označen jako defaultní a bude použit pro nové tenanty bez specifikovaného profilu.",
  2037 + "no-tenant-profiles-found": "Nebyly nalezeny žádné profily tenantů.",
  2038 + "create-new-tenant-profile": "Vytvořit nový!",
  2039 + "maximum-devices": "Maximální počet zařízení (0 - neomezeno)",
  2040 + "maximum-devices-required": "Maximální počet zařízení je povinný.",
  2041 + "maximum-devices-range": "Minimální počet zařízení nemůže být záporný",
  2042 + "maximum-assets": "Maximální počet aktiv (0 - neomezeno)",
  2043 + "maximum-assets-required": "Maximální počet aktiv je povinný.",
  2044 + "maximum-assets-range": "Maximální počet aktiv nemůže být záporný",
  2045 + "maximum-customers": "Maximální počet zákazníků (0 - neomezeno)",
  2046 + "maximum-customers-required": "Maximální počet zákazníkůje povinný.",
  2047 + "maximum-customers-range": "Maximální počet zákazníků nemůže být záporný",
  2048 + "maximum-users": "Maximální počet uživatelů (0 - neomezeno)",
  2049 + "maximum-users-required": "Maximální počet uživatelů je povinný.",
  2050 + "maximum-users-range": "Maximální počet uživatelů nemůže být záporný",
  2051 + "maximum-dashboards": "Maximální počet dashboardů (0 - neomezeno)",
  2052 + "maximum-dashboards-required": "Maximální počet dashboardů je povinný.",
  2053 + "maximum-dashboards-range": "Maximální počet dashboardů nemůže být záporný",
  2054 + "maximum-rule-chains": "Maximální počet řetězů pravidel (0 - neomezeno)",
  2055 + "maximum-rule-chains-required": "Maximální počet řetězů pravidel je povinný.",
  2056 + "maximum-rule-chains-range": "Maximální počet řetězů pravidel nemůže být záporný",
  2057 + "transport-tenant-msg-rate-limit": "Limit přenosu zpráv tenanta.",
  2058 + "transport-tenant-telemetry-msg-rate-limit": "Limit přenosu zpráv telemetrie tenanta.",
  2059 + "transport-tenant-telemetry-data-points-rate-limit": "Limit přenosu datových bodů telemetrie tenanta.",
  2060 + "transport-device-msg-rate-limit": "Limit přenosu zpráv zařízení.",
  2061 + "transport-device-telemetry-msg-rate-limit": "Limit přenosu zpráv zařízení telemetrie tenanta.",
  2062 + "transport-device-telemetry-data-points-rate-limit": "Limit přenosu datových bodů zařízení telemetrie tenanta.",
  2063 + "max-transport-messages": "Maximální počet zpráv přenosu (0 - neomezeno)",
  2064 + "max-transport-messages-required": "Maximální počet zpráv přenosu je povinný.",
  2065 + "max-transport-messages-range": "Maximální počet zpráv přenosu nemůže být záporný",
  2066 + "max-transport-data-points": "Maximální počet datových bodů přenosu (0 - neomezeno)",
  2067 + "max-transport-data-points-required": "Maximální počet datových bodů přenosu je povinný.",
  2068 + "max-transport-data-points-range": "Maximální počet datových bodů přenosu nemůže být záporný",
  2069 + "max-r-e-executions": "Maximální počet zpracování enginu pro zpracování pravidel (0 - neomezeno)",
  2070 + "max-r-e-executions-required": "Maximální počet zpracování enginu pro zpracování pravidel je povinný.",
  2071 + "max-r-e-executions-range": "Maximální počet zpracování enginu pro zpracování pravidel nemůže být záporný",
  2072 + "max-j-s-executions": "Maximální počet JavaScript zpracování (0 - neomezeno)",
  2073 + "max-j-s-executions-required": "Maximální počet JavaScript zpracování je povinný.",
  2074 + "max-j-s-executions-range": "Maximální počet JavaScript zpracování nemůže být záporný",
  2075 + "max-d-p-storage-days": "Maximální počet dnů uložení datových bodů (0 - neomezeno)",
  2076 + "max-d-p-storage-days-required": "Maximální počet dnů uložení datových bodů je povinný.",
  2077 + "max-d-p-storage-days-range": "Maximální počet dnů uložení datových bodů nemůže být záporný",
  2078 + "default-storage-ttl-days": "Defaultní počet dnů TTL úložiště (0 - neomezeno)",
  2079 + "default-storage-ttl-days-required": "Defaultní počet dnů TTL úložiště je povinný.",
  2080 + "default-storage-ttl-days-range": "Defaultní počet dnů TTL úložiště nemůže být záporný",
  2081 + "max-rule-node-executions-per-message": "Maximální počet zpracování uzlů pravidel na zprávu (0 - neomezeno)",
  2082 + "max-rule-node-executions-per-message-required": "Maximální počet zpracování uzlů pravidel na zprávu je povinný.",
  2083 + "max-rule-node-executions-per-message-range": "Maximální počet zpracování uzlů pravidel na zprávu nemůže být záporný",
  2084 + "max-emails": "Maximální počet odeslaných emailů (0 - neomezeno)",
  2085 + "max-emails-required": "Maximální počet odeslaných emailů je povinný.",
  2086 + "max-emails-range": "Maximální počet odeslaných emailů nemůže být záporný",
  2087 + "max-sms": "Maximální počet odeslaných SMS (0 - neomezeno)",
  2088 + "max-sms-required": "Maximální počet odeslaných SMS je povinný.",
  2089 + "max-sms-range": "Maximální počet odeslaných SMS nemůže být záporný"
  2090 + },
1566 2091 "timeinterval": {
1567 2092 "seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }",
1568 2093 "minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }",
... ... @@ -1574,8 +2099,14 @@
1574 2099 "seconds": "Vteřiny",
1575 2100 "advanced": "Rozšířené"
1576 2101 },
  2102 + "timeunit": {
  2103 + "seconds": "Vteřiny",
  2104 + "minutes": "Minuty",
  2105 + "hours": "Hodiny",
  2106 + "days": "Dny"
  2107 + },
1577 2108 "timewindow": {
1578   - "days": "{ days, plural, 1 { den } other {# days } }",
  2109 + "days": "{ days, plural, 1 { den } other {# d } }",
1579 2110 "hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }",
1580 2111 "minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }",
1581 2112 "seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }",
... ... @@ -1694,6 +2225,7 @@
1694 2225 "type": "Typ widgetu",
1695 2226 "resources": "Zdroje",
1696 2227 "resource-url": "JavaScript/CSS URL",
  2228 + "resource-is-module": "Je modulem",
1697 2229 "remove-resource": "Odebrat zdroj",
1698 2230 "add-resource": "Přidat zdroj",
1699 2231 "html": "HTML",
... ... @@ -1711,7 +2243,10 @@
1711 2243 "widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!",
1712 2244 "add": "Přidat widget",
1713 2245 "undo": "Vrátit změny widgetu",
1714   - "export": "Exportovat widget"
  2246 + "export": "Exportovat widget",
  2247 + "no-data": "Nejsou k dispozici žádná data pro zobrazení ve widgetu",
  2248 + "data-overflow": "Widget zobrazuje {{count}} z {{total}} entit",
  2249 + "alarm-data-overflow": "Widget zobrazuje alarmy {{allowedEntities}} (maxima možných) entit z {{totalEntities}} entit"
1715 2250 },
1716 2251 "widget-action": {
1717 2252 "header-button": "Tlačítko hlavičky widgetu",
... ... @@ -1724,7 +2259,14 @@
1724 2259 "target-dashboard-state-required": "Cílový stav dashboardu je povinný",
1725 2260 "set-entity-from-widget": "Nastavit entitu z widgetu",
1726 2261 "target-dashboard": "Cílový dashboard",
1727   - "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)"
  2262 + "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)",
  2263 + "open-in-separate-dialog": "Otevřít v samostatném okně",
  2264 + "dialog-title": "Název okna",
  2265 + "dialog-hide-dashboard-toolbar": "Skrýt v okně nástrojovou lištu dashboardu",
  2266 + "dialog-width": "Šířka okna v procentech vzhledem k šířce obrazovky",
  2267 + "dialog-height": "Výška okna v procentech vzhledem k výšce obrazovky",
  2268 + "dialog-size-range-error": "Hodnota procentuální velikosti musí být v rozsahu od 1 do 100.",
  2269 + "open-new-browser-tab": "Otevřít na nové záložce prohlížeče"
1728 2270 },
1729 2271 "widgets-bundle": {
1730 2272 "current": "Vybraná kategorie",
... ... @@ -1891,8 +2433,11 @@
1891 2433 "entity-coordinate-required": "Obě pole, zeměpisná šířka i zeměpisná délka, jsou povinná",
1892 2434 "entity-timeseries-required": "Časové řady entity jsou povinné",
1893 2435 "get-location": "Získat aktuální polohu",
  2436 + "invalid-date": "Neplatné datum",
1894 2437 "latitude": "Zeměpisná šířka",
1895 2438 "longitude": "Zeměpisná délka",
  2439 + "min-value-error": "Minimální hodnota je {{value}}",
  2440 + "max-value-error": "Maximální hodnota je {{value}}",
1896 2441 "not-allowed-entity": "Vybraná entita nemůže mít sdílené atributy",
1897 2442 "no-attribute-selected": "Není vybrán žádný atribut",
1898 2443 "no-datakey-selected": "Není vybrán žádný datový klíč",
... ... @@ -1900,7 +2445,10 @@
1900 2445 "no-entity-selected": "Není vybrána žádná entita",
1901 2446 "no-image": "Žádný obrázek",
1902 2447 "no-support-geolocation": "Váš prohlížeč nepodporuje geolokaci",
1903   - "no-support-web-camera": "Žádná podporovaná webová kamera",
  2448 + "no-support-web-camera": "Váš prohlížeč nepodporuje kamery",
  2449 + "enable-https-use-widget": "Prosím povolte HTTPS abyste mohli používat tento widget",
  2450 + "no-found-your-camera": "Nelze nalézt vyši kameru",
  2451 + "no-permission-camera": "Přístup byl zakázán uživatelem / Tato stránka nemá oprávnění použít kameru",
1904 2452 "no-timeseries-selected": "Nejsou vybrány žádné časové řady",
1905 2453 "secret-key": "Tajný klíč",
1906 2454 "secret-key-required": "Tajný klíč je povinný",
... ...
... ... @@ -74,6 +74,7 @@
74 74 "admin": {
75 75 "general": "General",
76 76 "general-settings": "General Settings",
  77 + "home-settings": "Home Settings",
77 78 "outgoing-mail": "Mail Server",
78 79 "outgoing-mail-settings": "Outgoing Mail Server Settings",
79 80 "system-settings": "System Settings",
... ... @@ -764,7 +765,9 @@
764 765 "select-state": "Select target state",
765 766 "state-controller": "State controller",
766 767 "search": "Search dashboards",
767   - "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected"
  768 + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected",
  769 + "home-dashboard": "Home dashboard",
  770 + "home-dashboard-hide-toolbar": "Hide home dashboard toolbar"
768 771 },
769 772 "datakey": {
770 773 "settings": "Settings",
... ...
... ... @@ -4,9 +4,11 @@
4 4 "unauthorized-access": "Acceso no autorizado",
5 5 "unauthorized-access-text": "Debes iniciar sesión para tener acceso a este recurso!",
6 6 "access-forbidden": "Acceso Prohibido",
7   - "access-forbidden-text": "No tienes derechos para acceder a esta ubicación!<br/>Intenta iniciar sesión con otro usuario si todavía quieres acceder a esta ubicación.",
  7 + "access-forbidden-text": "No tienes suficientes derechos para acceder a esta ubicación!<br/>Intenta iniciar sesión con otro usuario si todavía quieres acceder a esta ubicación.",
8 8 "refresh-token-expired": "La sesión ha expirado",
9   - "refresh-token-failed": "No se puede actualizar la sesión"
  9 + "refresh-token-failed": "No se puede actualizar la sesión",
  10 + "permission-denied": "Permiso Denegado",
  11 + "permission-denied-text": "No tienes suficientes derechos para realizar esta operación!"
10 12 },
11 13 "action": {
12 14 "activate": "Activar",
... ... @@ -21,6 +23,7 @@
21 23 "no": "No",
22 24 "update": "Actualizar",
23 25 "remove": "Eliminar",
  26 + "select": "Seleccionar",
24 27 "search": "Buscar",
25 28 "clear-search": "Borrar búsqueda",
26 29 "assign": "Asignar",
... ... @@ -49,13 +52,16 @@
49 52 "import": "Importar",
50 53 "export": "Exportar",
51 54 "share-via": "Compartir vía {{provider}}",
52   - "discard-changes": "Cancelar los cambios",
53 55 "continue": "Continuar",
54   - "download": "Descargar"
  56 + "discard-changes": "Cancelar cambios",
  57 + "download": "Descargar",
  58 + "next-with-label": "Siguiente: {{label}}",
  59 + "read-more": "Leer más",
  60 + "hide": "Ocultar"
55 61 },
56 62 "aggregation": {
57   - "aggregation": "Agregación",
58   - "function": "Función de Agregación",
  63 + "aggregation": "Agrupación",
  64 + "function": "Función de Agrupación",
59 65 "limit": "Valores Max",
60 66 "group-interval": "Intervalo de agrupación",
61 67 "min": "Min",
... ... @@ -74,6 +80,8 @@
74 80 "test-mail-sent": "Mail de prueba enviado correctamente!",
75 81 "base-url": "URL Base",
76 82 "base-url-required": "URL Base requerida.",
  83 + "prohibit-different-url": "Prohibir el uso de hostname en cabeceras de request del cliente",
  84 + "prohibit-different-url-hint": "Este ajuste debe ser activado en entornos de producción. Puede causar fallos de seguridad si está desactivado",
77 85 "mail-from": "Mail Desde",
78 86 "mail-from-required": "Mail Desde requerido.",
79 87 "smtp-protocol": "Protocolo SMTP",
... ... @@ -87,9 +95,44 @@
87 95 "timeout-invalid": "No parece un Timeout valido.",
88 96 "enable-tls": "Habilitar TLS",
89 97 "tls-version": "Versión TLS",
  98 + "enable-proxy": "Habilitar proxy",
  99 + "proxy-host": "Host proxy",
  100 + "proxy-host-required": "Se requiere host Proxy.",
  101 + "proxy-port": "Puerto proxy",
  102 + "proxy-port-required": "Se requiere puerto proxy.",
  103 + "proxy-port-range": "El puerto proxy debe estar en un rango de 1 a 65535.",
  104 + "proxy-user": "Usuario proxy",
  105 + "proxy-password": "Contraseña proxy",
90 106 "send-test-mail": "Enviar correo de prueba",
91   - "password-policy": "Política de contraseñas",
  107 + "sms-provider": "Proveedor SMS",
  108 + "sms-provider-settings": "Ajustes proveedor SMS",
  109 + "sms-provider-type": "Tipo de proveedor SMS",
  110 + "sms-provider-type-required": "Se requiere proveedor SMS.",
  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": "Se requiere AWS Access Key ID",
  115 + "aws-secret-access-key": "AWS Secret Access Key",
  116 + "aws-secret-access-key-required": "Se requere AWS Secret Access Key",
  117 + "aws-region": "Región AWS",
  118 + "aws-region-required": "Se requere región AWS",
  119 + "number-from": "Nº de teléfono Origen",
  120 + "number-from-required": "Se requere Nº de teléfono origen.",
  121 + "number-to": "Nº de teléfono de destino",
  122 + "number-to-required": "Se requere Nº de teléfono de destino.",
  123 + "phone-number-hint": "Nº de teléfono en formato E.164, ex. +19995550123",
  124 + "phone-number-pattern": "Nº Inválido. Debe estar en formato E.164, ex. +19995550123.",
  125 + "sms-message": "Mensaje SMS",
  126 + "sms-message-required": "Se requeriere mensaje SMS.",
  127 + "sms-message-max-length": "Los SMS no pueden ser más largos de 1600 caracteres",
  128 + "twilio-account-sid": "SID de cuenta Twilio",
  129 + "twilio-account-sid-required": "Se requere SID de cuenta Twilio",
  130 + "twilio-account-token": "Token de cuenta Twilio",
  131 + "twilio-account-token-required": "Se requiere Token Twilio",
  132 + "send-test-sms": "Enviar SMS de prueba",
  133 + "test-sms-sent": "SMS enviado con éxito!",
92 134 "security-settings": "Configuraciones de seguridad",
  135 + "password-policy": "Política de contraseñas",
93 136 "minimum-password-length": "Longitud mínima de contraseña",
94 137 "minimum-password-length-required": "Se requiere una longitud mínima de contraseña",
95 138 "minimum-password-length-range": "La longitud mínima de la contraseña debe estar en un rango de 5 a 50",
... ... @@ -108,8 +151,74 @@
108 151 "general-policy": "Política general",
109 152 "max-failed-login-attempts": "Número máximo de intentos fallidos de inicio de sesión, antes de que la cuenta esté bloqueada",
110 153 "minimum-max-failed-login-attempts-range": "El número máximo de intentos fallidos de inicio de sesión no puede ser negativo",
111   - "user-lockout-notification-email": "En caso de bloqueo de la cuenta del usuario, envíe una notificación por correo electrónico"
112   - },
  154 + "user-lockout-notification-email": "En caso de bloqueo de la cuenta del usuario, envíe una notificación por correo electrónico",
  155 + "domain-name": "Nombre de dominio",
  156 + "domain-name-unique": "El nombre de dominio y protocolo debe ser único.",
  157 + "error-verification-url": "Un nombre de dominio no debe contener símbolos '/' y ':'. Ejemplo: thingsboard.io",
  158 + "oauth2": {
  159 + "access-token-uri": "URI Access token",
  160 + "access-token-uri-required": "Se requere URI Access token.",
  161 + "activate-user": "Activar usuario",
  162 + "add-domain": "Añadir dominio",
  163 + "delete-domain": "Borrar dominio",
  164 + "add-provider": "Añadir proveedor",
  165 + "delete-provider": "Borrar proveedor",
  166 + "allow-user-creation": "Permitir creación de usuario",
  167 + "always-fullscreen": "Siempre pantalla completa",
  168 + "authorization-uri": "URI Autorización",
  169 + "authorization-uri-required": "Se requiere URI de Autorización.",
  170 + "client-authentication-method": "Método de autenticación",
  171 + "client-id": "ID Cliente",
  172 + "client-id-required": "Se requere ID Cliente.",
  173 + "client-secret": "Secreto de Cliente",
  174 + "client-secret-required": "Se requiere Secreto de Cliente.",
  175 + "custom-setting": "Ajustes personalizados",
  176 + "customer-name-pattern": "Patrón nombre de cliente",
  177 + "default-dashboard-name": "Nombre de panel por defecto",
  178 + "delete-domain-text": "Atención, tras la confirmación el dominio y todos los datos del proveedor no estarán disponibles.",
  179 + "delete-domain-title": "Eliminar los ajustes del dominio '{{domainName}}'?",
  180 + "delete-registration-text": "Atención, tras la confirmación los datos del proveedor no estarán disponibles.",
  181 + "delete-registration-title": "Eliminar el proveedor '{{name}}'?",
  182 + "email-attribute-key": "Clave de atributos email",
  183 + "email-attribute-key-required": "Se requiere clave de atributos de email.",
  184 + "first-name-attribute-key": "Clave de atributos de nombre",
  185 + "general": "General",
  186 + "jwk-set-uri": "URI web key JSON",
  187 + "last-name-attribute-key": "Clave de atributos de apellido",
  188 + "login-button-icon": "Icono de botón login",
  189 + "login-button-label": "Etiqueta de proveedor",
  190 + "login-button-label-placeholder": "Login con $(Provider label)",
  191 + "login-button-label-required": "Clave de etiqueta requerida.",
  192 + "login-provider": "Proveedor de login",
  193 + "mapper": "Mapeador",
  194 + "new-domain": "Nuevo dominio",
  195 + "oauth2": "OAuth2",
  196 + "redirect-uri-template": "Plantilla de redirección URI",
  197 + "copy-redirect-uri": "Copiar URI de redirección",
  198 + "registration-id": "ID de registro",
  199 + "registration-id-required": "Se requiere ID de registro.",
  200 + "registration-id-unique": "El ID de registro debe ser único en el sistema.",
  201 + "scope": "Alcance",
  202 + "scope-required": "Se requiere alcance.",
  203 + "tenant-name-pattern": "Patrón de nombre de propietario",
  204 + "tenant-name-pattern-required": "Se requiere patrón de nombre de propietario.",
  205 + "tenant-name-strategy": "Estrategia de Nombre de Propietario",
  206 + "type": "Tipo de mapeador",
  207 + "uri-pattern-error": "Formato de URI inválido.",
  208 + "url": "URL",
  209 + "url-pattern": "Formato URL inválido.",
  210 + "url-required": "Se requiere URL.",
  211 + "user-info-uri": "URI Información de usuario",
  212 + "user-info-uri-required": "Se requiere URI de información usuario.",
  213 + "user-name-attribute-name": "Clave de atributos de nombre de usuario",
  214 + "user-name-attribute-name-required": "Se requiere clave de atributos de nombre de usuario",
  215 + "protocol": "Protocolo",
  216 + "domain-schema-http": "HTTP",
  217 + "domain-schema-https": "HTTPS",
  218 + "domain-schema-mixed": "HTTP+HTTPS",
  219 + "enable": "Activar ajustes OAuth2"
  220 + }
  221 + },
113 222 "alarm": {
114 223 "alarm": "Alarma",
115 224 "alarms": "Alarmas",
... ... @@ -117,6 +226,8 @@
117 226 "no-alarms-matching": "No se han encontrado alarmas coincidentes con '{{entity}}' .",
118 227 "alarm-required": "Alarma requerida",
119 228 "alarm-status": "Estado de Alarma",
  229 + "alarm-status-list": "Lista de estados de Alarmas",
  230 + "any-status": "Cualquier estado",
120 231 "search-status": {
121 232 "ANY": "Todas",
122 233 "ACTIVE": "Activas",
... ... @@ -143,6 +254,8 @@
143 254 "end-time": "Hora fin",
144 255 "ack-time": "Hora de reconocimiento",
145 256 "clear-time": "Hora de normalización",
  257 + "alarm-severity-list": "Lista de gravedad de alarmas",
  258 + "any-severity": "Cualquier gravedad",
146 259 "severity-critical": "Crítica",
147 260 "severity-major": "Mayor",
148 261 "severity-minor": "Menor",
... ... @@ -158,19 +271,23 @@
158 271 "min-polling-interval-message": "El ciclo debe ser por lo menos de 1 segundo.",
159 272 "aknowledge-alarms-title": "Reconocer { count, plural, 1 {1 alarma} other {# alarmas} }",
160 273 "aknowledge-alarms-text": "Estas seguro de reconocer { count, plural, 1 {1 alarma} other {# alarmas} }?",
161   - "aknowledge-alarm-title": "Recononcer Alarma",
  274 + "aknowledge-alarm-title": "Recononcer Alarma",
162 275 "aknowledge-alarm-text": "Estas seguro de reconocer Alarma?",
163 276 "clear-alarms-title": "Normalizar { count, plural, 1 {1 alarma} other {# alarmas} }",
164   - "clear-alarms-text": "Estás seguro de limpiar { count, plural, 1 {1 alarma} other {# alarmas} }?",
165   - "clear-alarm-title": "Limpiar Alarma",
166   - "clear-alarm-text": "Estás seguro de limpiar Alarma?",
167   - "alarm-status-filter": "Filtro de Alarmas",
  277 + "clear-alarms-text": "Limpiar { count, plural, 1 {1 alarma} other {# alarmas} }?",
  278 + "clear-alarm-title": "Limpiar Alarma",
  279 + "clear-alarm-text": "Limpiar Alarma?",
  280 + "alarm-status-filter": "Filtro de estados de Alarmas",
  281 + "alarm-filter": "Filtro de Alarmas",
168 282 "max-count-load": "Número máximo de alarmas a cargar (0 - ilimitado)",
169 283 "max-count-load-required": "Se requiere número máximo de alarmas.",
170 284 "max-count-load-error-min": "El valor mínimo es 0.",
171 285 "fetch-size": "Tamaño de búsqueda (Fetch)",
172 286 "fetch-size-required": "Se requiere tamaño de búsqueda.",
173   - "fetch-size-error-min": "El valor mínimo es 10."
  287 + "fetch-size-error-min": "El valor mínimo es 10.",
  288 + "alarm-type-list": "Lista de tipos de alarma",
  289 + "any-type": "Cualquier tipo",
  290 + "search-propagated-alarms": "Buscar alarmas propagadas"
174 291 },
175 292 "alias": {
176 293 "add": "Añadir alias",
... ... @@ -200,6 +317,7 @@
200 317 "filter-type-device-search-query-description": "Dispositivos con tipos {{deviceTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}",
201 318 "filter-type-entity-view-search-query": "Consulta de búsqueda de vista de entidad",
202 319 "filter-type-entity-view-search-query-description": "Vistas de entidad con tipos {{entityViewTypes}} que tienen tipo de relación {{relationType}} con dirección {{direction}} {{rootEntity}}",
  320 + "filter-type-apiUsageState": "Uso de API",
203 321 "entity-filter": "Filtro por entidad",
204 322 "resolve-multiple": "Tomar como múltiples entidades",
205 323 "filter-type": "Filtro por tipo",
... ... @@ -259,19 +377,19 @@
259 377 "unassign-assets": "Cancelar asignación de activo",
260 378 "unassign-assets-action-title": "Cancelar asignación de { count, plural, 1 {1 activo} other {# activos} } del cliente",
261 379 "assign-new-asset": "Asignar nuevo activo",
262   - "delete-asset-title": "Estás seguro de borrar el activo '{{assetName}}'?",
  380 + "delete-asset-title": "Eliminar el activo '{{assetName}}'?",
263 381 "delete-asset-text": "Atención, tras la confirmación el activo y sus datos serán borrados e irrecuperables.",
264   - "delete-assets-title": "Estás seguro de borrar los activos { count, plural, 1 {1 activo} other {# activos} }?",
  382 + "delete-assets-title": "Eliminar los activos { count, plural, 1 {1 activo} other {# activos} }?",
265 383 "delete-assets-action-title": "Borrar { count, plural, 1 {1 activo} other {# activos} }",
266 384 "delete-assets-text": "Atención, tras la confirmación todos los activos seleccionados y sus datos serán borrados e irrecuperables.",
267   - "make-public-asset-title": "Estás seguro de hacer el activo '{{assetName}}' público?",
  385 + "make-public-asset-title": "Hacer el activo '{{assetName}}' público?",
268 386 "make-public-asset-text": "Tras la confirmación, el activo y sus datos se harán públicos y accesibles por otros.",
269   - "make-private-asset-title": "Estás seguro de hacer el activo '{{assetName}}' privado?",
  387 + "make-private-asset-title": "Hacer el activo '{{assetName}}' privado?",
270 388 "make-private-asset-text": "Tras la confirmación, el activo y sus datos se harán privados y no serán accesibles por otros.",
271   - "unassign-asset-title": "Estás seguro de cancelar la asignación del activo '{{assetName}}'?",
  389 + "unassign-asset-title": "Cancelar la asignación del activo '{{assetName}}'?",
272 390 "unassign-asset-text": "Tras la confirmación, el activo será desasignado y no será accesible por el cliente.",
273 391 "unassign-asset": "Cancelar asignación de activo",
274   - "unassign-assets-title": "Estás seguro de cancelar las asignaciones { count, plural, 1 {1 activo} other {# activos} }?",
  392 + "unassign-assets-title": "Cancelar las asignaciones { count, plural, 1 {1 activo} other {# activos} }?",
275 393 "unassign-assets-text": "Tras la confirmación todos los activos seleccionados serán desasignados y no serán accesibles por el cliente.",
276 394 "copyId": "Copiar ID de activo",
277 395 "idCopiedMessage": "El ID ha sido copiado al portapapeles",
... ... @@ -281,6 +399,8 @@
281 399 "name-starts-with": "El nombre de activo comienza con",
282 400 "import": "Importar activos",
283 401 "asset-file": "Archivo del activo",
  402 + "search": "Buscar activos",
  403 + "selected-assets": "{ count, plural, 1 {1 activo} other {# activos} } seleccionados",
284 404 "label": "Etiqueta"
285 405 },
286 406 "attribute": {
... ... @@ -297,7 +417,7 @@
297 417 "key-required": "Clave del atributo requerida.",
298 418 "value": "Valor",
299 419 "value-required": "Valor del atributo requerido.",
300   - "delete-attributes-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 atributo} other {# atributos} }?",
  420 + "delete-attributes-title": "¿Eliminar { count, plural, 1 {1 atributo} other {# atributos} }?",
301 421 "delete-attributes-text": "Atención, tras la confirmación el atributo será eliminado, y la información relacionada será irrecuperable.",
302 422 "delete-attributes": "Borrar atributo",
303 423 "enter-attribute-value": "Ingresar valor del atributo",
... ... @@ -308,7 +428,62 @@
308 428 "add-to-dashboard": "Agregar al Panel",
309 429 "add-widget-to-dashboard": "Agregar widget al Panel",
310 430 "selected-attributes": "{ count, plural, 1 {1 atributo} other {# atributos} } seleccionados",
311   - "selected-telemetry": "{ count, plural, 1 {1 telemetría} other {# telemetrías} } seleccionadas"
  431 + "selected-telemetry": "{ count, plural, 1 {1 telemetría} other {# telemetrías} } seleccionadas",
  432 + "no-attributes-text": "No se encontró ningún atributo",
  433 + "no-telemetry-text": "No se encontró ninguna telemetría"
  434 + },
  435 + "api-usage": {
  436 + "api-usage": "Uso de API",
  437 + "data-points": "Puntos de datos",
  438 + "data-points-storage-days": "Días de grabación de puntos de datos",
  439 + "email": "Email",
  440 + "email-messages": "Mensajes de Email",
  441 + "email-messages-daily-activity": "Actividad diaria de Emails",
  442 + "email-messages-hourly-activity": "Actividad horaria de Emails",
  443 + "email-messages-monthly-activity": "Actividad mensual de Emails",
  444 + "exceptions": "Excepciones",
  445 + "executions": "Ejecuciones",
  446 + "javascript": "JavaScript",
  447 + "javascript-executions": "Ejecuciones JavaScript",
  448 + "javascript-functions": "Funciones JavaScript",
  449 + "javascript-functions-daily-activity": "Actividad diaria de funciones JavaScript",
  450 + "javascript-functions-hourly-activity": "Actividad horaria de funciones JavaScript",
  451 + "javascript-functions-monthly-activity": "Actividad mensual de funciones JavaScript",
  452 + "latest-error": "Último error",
  453 + "messages": "Mensajes",
  454 + "permanent-failures": "${entityName} Fallos permanentes",
  455 + "permanent-timeouts": "${entityName} Timeouts permanentes",
  456 + "processing-failures": "${entityName} Fallos de procesamiento",
  457 + "processing-failures-and-timeouts": "Fallos de procesamiento y timeouts",
  458 + "processing-timeouts": "${entityName} Timeouts de procesamiento",
  459 + "queue-stats": "Estadísticas de colas",
  460 + "rule-chain": "Cadena de reglas",
  461 + "rule-engine": "Motor de reglas",
  462 + "rule-engine-daily-activity": "Actividad diaria de motor de reglas",
  463 + "rule-engine-executions": "Ejecuciones de motor de reglas",
  464 + "rule-engine-hourly-activity": "Actividad horaria de motor de reglas",
  465 + "rule-engine-monthly-activity": "Actividad mensual de motor de reglas",
  466 + "rule-engine-statistics": "Estadisticas del motor de reglas",
  467 + "rule-node": "Nodo de reglas",
  468 + "sms": "SMS",
  469 + "sms-messages": "Mensajes SMS",
  470 + "sms-messages-daily-activity": "Actividad diaria de mensajes SMS",
  471 + "sms-messages-hourly-activity": "Actividad horaria de mensajes SMS",
  472 + "sms-messages-monthly-activity": "Actividad mensual de mensajes SMS",
  473 + "successful": "${entityName} Exitoso",
  474 + "telemetry": "Telemetría",
  475 + "telemetry-persistence": "Persistencia de telemetría",
  476 + "telemetry-persistence-daily-activity": "Actividad diaria de persistencia de telemetría",
  477 + "telemetry-persistence-hourly-activity": "Actividad horaria de persistencia de telemetría",
  478 + "telemetry-persistence-monthly-activity": "Actividad mensual de persistencia de telemetría",
  479 + "transport": "Transporte",
  480 + "transport-daily-activity": "Actividad diaria de transporte",
  481 + "transport-data-points": "Puntos de datos de transporte",
  482 + "transport-hourly-activity": "Actividad horaria de transporte",
  483 + "transport-messages": "Mensajes de transporte",
  484 + "transport-monthly-activity": "Actividad mensual de transporte",
  485 + "view-details": "Ver detalles",
  486 + "view-statistics": "Ver estadísticas"
312 487 },
313 488 "audit-log": {
314 489 "audit": "Auditoría",
... ... @@ -348,11 +523,17 @@
348 523 "action-data": "Datos de acción",
349 524 "failure-details": "Detalles del error",
350 525 "search": "Buscar registros de auditoría",
351   - "clear-search": "Borrar búsqueda"
  526 + "clear-search": "Borrar búsqueda",
  527 + "type-assigned-from-tenant": "Asignado desde el administrador",
  528 + "type-assigned-to-tenant": "Asignado al administrador",
  529 + "type-provision-success": "Dispositivo aprovisionado",
  530 + "type-provision-failure": "Aprovisionamiento fallido",
  531 + "type-timeseries-updated": "Telemetría actualizada",
  532 + "type-timeseries-deleted": "Telemetría borrada"
352 533 },
353 534 "confirm-on-exit": {
354   - "message": "Tienes cambios sin guardar. ¿Estás seguro que quieres abandonar la página?",
355   - "html-message": "Tienes cambios sin guardar.<br/>¿Estás seguro que quieres abandonar la página?",
  535 + "message": "Tienes cambios sin guardar. ¿Abandonar la página?",
  536 + "html-message": "Tienes cambios sin guardar.<br/>¿Abandonar la página?",
356 537 "title": "Cambios sin guardar"
357 538 },
358 539 "contact": {
... ... @@ -372,7 +553,9 @@
372 553 "password": "Contraseña",
373 554 "enter-username": "Introduce el nombre de usuario.",
374 555 "enter-password": "Introduce la contraseña",
375   - "enter-search": "Introduce búsqueda"
  556 + "enter-search": "Introduce búsqueda",
  557 + "created-time": "Fecha de creación",
  558 + "loading": "Cargando..."
376 559 },
377 560 "content-type": {
378 561 "json": "Json",
... ... @@ -404,9 +587,9 @@
404 587 "add-customer-text": "Agregar nuevo cliente",
405 588 "no-customers-text": "No se encontraron clientes",
406 589 "customer-details": "Detalles del cliente",
407   - "delete-customer-title": "¿Estás seguro que quieres eliminar el cliente '{{customerTitle}}'?",
  590 + "delete-customer-title": "¿Eliminar el cliente '{{customerTitle}}'?",
408 591 "delete-customer-text": "Atención, tras la confirmación el cliente será eliminado y toda la información relacionada será irrecuperable.",
409   - "delete-customers-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 cliente} other {# clientes} }?",
  592 + "delete-customers-title": "¿Eliminar { count, plural, 1 {1 cliente} other {# clientes} }?",
410 593 "delete-customers-action-title": "Borrar { count, plural, 1 {1 cliente} other {# clientes} }",
411 594 "delete-customers-text": "Atención, tras la confirmación todos los clientes seleccionados serán eliminados y su información relacionada será irrecuperable.",
412 595 "manage-users": "Gestionar usuarios",
... ... @@ -425,7 +608,9 @@
425 608 "customer-required": "Cliente requerido",
426 609 "select-default-customer": "Seleccionar cliente por defecto",
427 610 "default-customer": "Cliente por defecto",
428   - "default-customer-required": "Se requiere cliente por defecto para realizar debug a nivel de propietario"
  611 + "default-customer-required": "Se requiere cliente por defecto para realizar debu a nivel de propietario",
  612 + "search": "Buscar clientes",
  613 + "selected-customers": "{ count, plural, 1 {1 cliente} other {# clientes} } seleccionados"
429 614 },
430 615 "datetime": {
431 616 "date-from": "Fecha desde",
... ... @@ -471,20 +656,20 @@
471 656 "delete-dashboards": "Eliminar paneles",
472 657 "unassign-dashboards": "Desasignar paneles",
473 658 "unassign-dashboards-action-title": "Desasignar { count, plural, 1 {1 paneles} other {# paneles} } del cliente",
474   - "delete-dashboard-title": "¿Estás seguro que quieres eliminar el panel '{{dashboardTitle}}'?",
  659 + "delete-dashboard-title": "¿Eliminar el panel '{{dashboardTitle}}'?",
475 660 "delete-dashboard-text": "Atención, el panel seleccionado será eliminado y la información relacionada sera irrecuperable.",
476   - "delete-dashboards-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 panel} other {# paneles} }?",
  661 + "delete-dashboards-title": "¿Eliminar { count, plural, 1 {1 panel} other {# paneles} }?",
477 662 "delete-dashboards-action-title": "Eliminar { count, plural, 1 {1 panel} other {# paneles} }",
478 663 "delete-dashboards-text": "Atención, los paneles seleccionados serán eliminados y la información relacionada será irrecuperable.",
479   - "unassign-dashboard-title": "¿Estás seguro que quieres desasignar el panel '{{dashboardTitle}}'?",
  664 + "unassign-dashboard-title": "¿Desasignar el panel '{{dashboardTitle}}'?",
480 665 "unassign-dashboard-text": "Tras la confirmación, el panel será desasignado y no podrá ser accesible por el cliente.",
481 666 "unassign-dashboard": "Desasignar panel",
482   - "unassign-dashboards-title": "¿Estás seguro que quieres desasignar { count, plural, 1 {1 panel} other {# paneles} }?",
  667 + "unassign-dashboards-title": "¿Desasignar { count, plural, 1 {1 panel} other {# paneles} }?",
483 668 "unassign-dashboards-text": "Atención, tras la confirmación los paneles seleccionados serán desasignados y no podrán ser accesibles por el cliente.",
484 669 "public-dashboard-title": "El panel ahora es público",
485 670 "public-dashboard-text": "Tu panel <b>{{dashboardTitle}}</b> es ahora público y podrá ser accedido desde: <a href='{{publicLink}}' target='_blank'>aquí</a>:",
486 671 "public-dashboard-notice": "<b>Nota:</b> No olvides hacer públicos los dispositivos relacionados para acceder a sus datos.",
487   - "make-private-dashboard-title": "¿Estás seguro que quieres hacer el panel '{{dashboardTitle}}' privado?",
  672 + "make-private-dashboard-title": "¿Hacer el panel '{{dashboardTitle}}' privado?",
488 673 "make-private-dashboard-text": "Tras la confirmación, el panel será privado y no podrá ser accesible por otros.",
489 674 "make-private-dashboard": "Hacer panel privado",
490 675 "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
... ... @@ -508,6 +693,9 @@
508 693 "min-columns-count-message": "Solo se permite un número mínimo de 10 columnas.",
509 694 "max-columns-count-message": "Solo se permite un número máximo de 1000 columnas.",
510 695 "widgets-margins": "Margen entre widgets",
  696 + "margin-required": "Valor de margen requerido.",
  697 + "min-margin-message": "0 es el valor de margen mínimo permitido.",
  698 + "max-margin-message": "50 es el valor de margen máximo permitido.",
511 699 "horizontal-margin": "Margen horizontal",
512 700 "horizontal-margin-required": "Margen horizontal requerido.",
513 701 "min-horizontal-margin-message": "Solo se permite margen horizontal mínimo de 0.",
... ... @@ -527,6 +715,7 @@
527 715 "title-color": "Color del título",
528 716 "display-dashboards-selection": "Mostrar selección de paneles",
529 717 "display-entities-selection": "Mostrar selección de entidades",
  718 + "display-filters": "Mostrar filtros",
530 719 "display-dashboard-timewindow": "Mostrar ventana de tiempo",
531 720 "display-dashboard-export": "Mostrar exportar",
532 721 "import": "Importar panel",
... ... @@ -560,6 +749,7 @@
560 749 "edit-state": "Editar estado panel",
561 750 "delete-state": "Borrar estado panel",
562 751 "add-state": "Añadir estado panel",
  752 + "no-states-text": "No se han encontrado estados",
563 753 "state": "Estado de panel",
564 754 "state-name": "Nombre",
565 755 "state-name-required": "Se requiere nombre del estado.",
... ... @@ -568,11 +758,13 @@
568 758 "state-id-exists": "Ya existe un ID de estado.",
569 759 "is-root-state": "Estado raiz(Root)",
570 760 "delete-state-title": "Borrar estado de panel",
571   - "delete-state-text": "Estás seguro de eliminar el estado de panel con nombre: '{{stateName}}'?",
  761 + "delete-state-text": "Eliminar el estado de panel con nombre: '{{stateName}}'?",
572 762 "show-details": "Mostrar detalles",
573 763 "hide-details": "Ocultar detalles",
574 764 "select-state": "Seleccionar estado destino (target state)",
575   - "state-controller": "Controlador de estados"
  765 + "state-controller": "Controlador de estados",
  766 + "search": "Buscar paneles",
  767 + "selected-dashboards": "{ count, plural, 1 {1 panel} other {# paneles} } seleccionados"
576 768 },
577 769 "datakey": {
578 770 "settings": "Ajustes",
... ... @@ -590,6 +782,7 @@
590 782 "alarm": "Campos de alarma",
591 783 "timeseries-required": "Series de tiempo del dispositivo requerido.",
592 784 "timeseries-or-attributes-required": "Series de tiempo/Atributos requeridos.",
  785 + "alarm-fields-timeseries-or-attributes-required": "Se requieren campos de alarma o series de tiempo/atributos.",
593 786 "maximum-timeseries-or-attributes": "Máximo { count, plural, 1 {1 timeseries/atributo es permitido.} other {# timeseries/atributos son permitidos} }",
594 787 "alarm-fields-required": "Campos de alarma requeridos.",
595 788 "function-types": "Tipos de funciones",
... ... @@ -607,6 +800,7 @@
607 800 "add-datasource-prompt": "Por favor, agrega una fuente de datos"
608 801 },
609 802 "details": {
  803 + "details": "Detalles",
610 804 "edit-mode": "Modo Edición",
611 805 "edit-json": "Editar JSON",
612 806 "toggle-edit-mode": "Ir a Modo Edición"
... ... @@ -658,20 +852,20 @@
658 852 "unassign-devices": "Desasignar dispositivos",
659 853 "unassign-devices-action-title": "Desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } del cliente",
660 854 "assign-new-device": "Asignar nuevo dispositivo",
661   - "make-public-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' público?",
  855 + "make-public-device-title": "¿Hacer el dispositivo '{{deviceName}}' público?",
662 856 "make-public-device-text": "Tras la confirmación, el dispositivo y la información relacionada serán públicos y podrá ser accesible por otros.",
663   - "make-private-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' privado?",
  857 + "make-private-device-title": "¿Hacer el dispositivo '{{deviceName}}' privado?",
664 858 "make-private-device-text": "Tras la confirmación, el dispositivo y la información relacionada serán privados y no podrá ser accesible por otros.",
665 859 "view-credentials": "Ver credenciales",
666   - "delete-device-title": "¿Estás seguro que quieres eliminar el dispositivo '{{deviceName}}'?",
  860 + "delete-device-title": "¿Eliminar el dispositivo '{{deviceName}}'?",
667 861 "delete-device-text": "Atención, tras la confirmación los dispositivos serán eliminados y la información relacionada será irrecuperable.",
668   - "delete-devices-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
  862 + "delete-devices-title": "¿Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
669 863 "delete-devices-action-title": "Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }",
670 864 "delete-devices-text": "Atención, tras la confirmación los dispositivos seleccionados serán eliminados y la información relacionada será irrecuperable.",
671   - "unassign-device-title": "¿Estás seguro que quieres desasignar el dispositivo '{{deviceName}}'?",
  865 + "unassign-device-title": "¿Desasignar el dispositivo '{{deviceName}}'?",
672 866 "unassign-device-text": "Tras la confirmación, el dispositivo será desasignado y no podrá ser accesible por el cliente.",
673 867 "unassign-device": "Desasignar dispositivo",
674   - "unassign-devices-title": "¿Estás seguro que quieres desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
  868 + "unassign-devices-title": "¿Desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
675 869 "unassign-devices-text": "Tras la confirmación, los dispositivos seleccionados serán desasignados y no podrán ser accedidos por el cliente.",
676 870 "device-credentials": "Credenciales del dispositivo",
677 871 "credentials-type": "Tipo de credencial",
... ... @@ -680,6 +874,12 @@
680 874 "access-token-invalid": "Access token debe tener entre 1 a 20 caracteres.",
681 875 "rsa-key": "Clave pública RSA",
682 876 "rsa-key-required": "Clave pública RSA requerida.",
  877 + "client-id": "ID Cliente",
  878 + "client-id-pattern": "Contiene carácter inválido.",
  879 + "user-name": "Nombre Usuario",
  880 + "user-name-required": "Se requiere nombre de usuario.",
  881 + "client-id-or-user-name-necessary": "El ID Cliente y/o el Nombre de usuario son necesarios",
  882 + "password": "Contraseña",
683 883 "secret": "Secreta",
684 884 "secret-required": "Secreta requerida.",
685 885 "device-type": "Tipo de dispositivo",
... ... @@ -692,25 +892,191 @@
692 892 "device-types": "Tipos de dispositivo",
693 893 "name": "Nombre",
694 894 "name-required": "El nombre es requerido.",
695   - "label": "Etiqueta",
696 895 "description": "Descripción",
  896 + "label": "Etiqueta",
697 897 "events": "Eventos",
698 898 "details": "Detalles",
699 899 "copyId": "Copiar ID",
700 900 "copyAccessToken": "Copiar access token",
  901 + "copy-mqtt-authentication": "Copiar credenciales MQTT",
701 902 "idCopiedMessage": "Id del dispositivo copiado al portapapeles",
702 903 "accessTokenCopiedMessage": "Access token del dispositivo copiado al portapapeles",
  904 + "mqtt-authentication-copied-message": "Los datos de autenticación MQTT se han copiado al portapapeles",
703 905 "assignedToCustomer": "Asignado al cliente",
704 906 "unable-delete-device-alias-title": "Imposible eliminar alias del dispositivo",
705 907 "unable-delete-device-alias-text": "Alias '{{deviceAlias}}' no puede ser eliminado. Esta siendo usado por el(los) widget(s):<br/>{{widgetsList}}",
706 908 "is-gateway": "Es gateway",
  909 + "overwrite-activity-time": "Sobreescribir hora de actividad para el dispositivo conectado",
707 910 "public": "Público",
708 911 "device-public": "El dispositivo es público",
709 912 "select-device": "Seleccionar dispositivo",
  913 + "import": "Importar dispositivo",
710 914 "device-file": "Archivo de dispositivo",
711   - "import": "Importar dispositivo"
  915 + "search": "Buscar dispositivos",
  916 + "selected-devices": "{ count, plural, 1 {1 dispositivo} other {# dispositivos} } seleccionados",
  917 + "device-configuration": "Configuración del dispositivo",
  918 + "transport-configuration": "Configuración del transporte",
  919 + "wizard": {
  920 + "device-wizard": "Asistente de dispositivo",
  921 + "device-details": "Detalles del dispositivo",
  922 + "new-device-profile": "Crear un nuevo perfil de dispositivo",
  923 + "existing-device-profile": "Seleccionar un perfil existente",
  924 + "specific-configuration": "Configuración específica",
  925 + "customer-to-assign-device": "Cliente al que asignar el dispositivo",
  926 + "add-credential": "Añadir credencial"
  927 + }
  928 + },
  929 + "device-profile": {
  930 + "device-profile": "Perfil de dispositivo",
  931 + "device-profiles": "Perfiles de dispositivo",
  932 + "all-device-profiles": "Todos",
  933 + "add": "Añadir perfil de dispositivo",
  934 + "edit": "Editar perfil de dispositivo",
  935 + "device-profile-details": "Detalles de perfil de dispositivo",
  936 + "no-device-profiles-text": "No se encontraron perfiles",
  937 + "search": "Buscar perfiles",
  938 + "selected-device-profiles": "{ count, plural, 1 {1 perfil} other {# perfiles} } seleccionados",
  939 + "no-device-profiles-matching": "No existe perfil que conincida con '{{entity}}'.",
  940 + "device-profile-required": "Se requiere perfil de dispositivo",
  941 + "idCopiedMessage": "Se ha copiado el ID de perfil al portapapeles",
  942 + "set-default": "Hacer perfil por defecto",
  943 + "delete": "Borrar perfil de dispositivo",
  944 + "copyId": "Copiar ID de perfil",
  945 + "new-device-profile-name": "Nombre de perfil",
  946 + "new-device-profile-name-required": "Se requiere nombre de perfil.",
  947 + "name": "Nombre",
  948 + "name-required": "Se requiere nombre.",
  949 + "type": "Tipo de perfil",
  950 + "type-required": "Se requiere tipo de perfil.",
  951 + "type-default": "Por defecto",
  952 + "transport-type": "Tipo de transporte",
  953 + "transport-type-required": "Se requiere tipo de transporte.",
  954 + "transport-type-default": "Por defecto",
  955 + "transport-type-default-hint": "Soporta transportes por MQTT básico, HTTP y CoAP",
  956 + "transport-type-mqtt": "MQTT",
  957 + "transport-type-mqtt-hint": "Activa ajustes avanzados de transporte MQTT",
  958 + "transport-type-lwm2m": "LWM2M",
  959 + "transport-type-lwm2m-hint": "Transporte LWM2M",
  960 + "description": "Descripción",
  961 + "default": "Defecto",
  962 + "profile-configuration": "Configuración de perfil",
  963 + "transport-configuration": "Configuración de transporte",
  964 + "default-rule-chain": "Cadena de reglas por defecto",
  965 + "select-queue-hint": "Selecciona desde el desplegable o añade un nombre personalizado.",
  966 + "delete-device-profile-title": "Eliminar el perfil '{{deviceProfileName}}'?",
  967 + "delete-device-profile-text": "Atención, tras la confirmación el perfil y todos sus datos serán borrados e irrecuperables.",
  968 + "delete-device-profiles-title": "EEliminar { count, plural, 1 {1 perfil} other {# perfiles} }?",
  969 + "delete-device-profiles-text": "Atención, tras la confirmación los perfiles seleccionados y todos sus datos serán borrados e irrecuperables.",
  970 + "set-default-device-profile-title": "Establecer el perfil '{{deviceProfileName}}' como perfil por defecto?",
  971 + "set-default-device-profile-text": "Tras la confirmación, el perfil será marcado como por defecto y será usado por todos los nuevos dispositivos que no tengan perfil especificado.",
  972 + "no-device-profiles-found": "No se encontraron perfiles.",
  973 + "create-new-device-profile": "Crear un nuevo perfil!",
  974 + "mqtt-device-topic-filters": "Filtros de topic MQTT",
  975 + "mqtt-device-topic-filters-unique": "Los filtros de topic de dispositivo MQTT deben ser únicos.",
  976 + "mqtt-device-payload-type": "Payload de dispositivo MQTT",
  977 + "mqtt-device-payload-type-json": "JSON",
  978 + "mqtt-device-payload-type-proto": "Protobuf",
  979 + "mqtt-payload-type-required": "Se requiere tipo de Payload.",
  980 + "support-level-wildcards": "Se soportan los wilcards únicos <code>[+]</code> y multi-nivel <code>[#]</code>.",
  981 + "telemetry-topic-filter": "Filtro de topic en telemetría",
  982 + "telemetry-topic-filter-required": "Se requiere filtro de topic (telemetría).",
  983 + "attributes-topic-filter": "Filtro de topic en atributos",
  984 + "attributes-topic-filter-required": "Se requiere filtro de topic (atributos).",
  985 + "telemetry-proto-schema": "Proto schema de telemetría",
  986 + "telemetry-proto-schema-required": "Se requiere proto schema de telemetría.",
  987 + "attributes-proto-schema": "Proto schema de atributos",
  988 + "attributes-proto-schema-required": "Se requiere proto schema de atributos.",
  989 + "rpc-response-topic-filter": "Filtro de topic de respuesta RPC",
  990 + "rpc-response-topic-filter-required": "Se requiere fitro de respuesta RPC.",
  991 + "not-valid-pattern-topic-filter": "No es un patrón de filtro válido",
  992 + "not-valid-single-character": "Uso inválido de wildcard único",
  993 + "not-valid-multi-character": "Uso inválido de wildcard multi-nivel",
  994 + "single-level-wildcards-hint": "<code>[+]</code> es adecuado para cualquier nivel. Ej.: <b>v1/devices/+/telemetry</b> o <b>+/devices/+/attributes</b>.",
  995 + "multi-level-wildcards-hint": "<code>[#]</code> puede reemplazar el mismo filtro y debe ser el último símbolo del topic. Ej.: <b>#</b> o <b>v1/devices/me/#</b>.",
  996 + "alarm-rules": "Reglas de alarma",
  997 + "alarm-rules-with-count": "Reglas de alarma ({{count}})",
  998 + "no-alarm-rules": "No hay reglas de alarma configuradas",
  999 + "add-alarm-rule": "Añadir regla de alarma",
  1000 + "edit-alarm-rule": "Editar regla de alarma",
  1001 + "alarm-type": "Tipo de alarma",
  1002 + "alarm-type-required": "Se requiere tipo de alarma.",
  1003 + "alarm-type-unique": "El tipo de alarma, debe ser único dentro de las reglas de alarma del perfil de dispositivo.",
  1004 + "create-alarm-pattern": "Crear alarma <b>{{alarmType}}</b>",
  1005 + "create-alarm-rules": "Crear reglas de alarma",
  1006 + "no-create-alarm-rules": "No hay condiciones de creación de alarma configuradas",
  1007 + "add-create-alarm-rule-prompt": "Por favor, añade una regla de alarma",
  1008 + "clear-alarm-rule": "Borrar regla de alarma",
  1009 + "no-clear-alarm-rule": "No hay condiciones de borrado de alarma configuradas",
  1010 + "add-create-alarm-rule": "Añadir crear condición (activar alarma)",
  1011 + "add-clear-alarm-rule": "Añair borrar condición (limpiar alarma)",
  1012 + "select-alarm-severity": "Selecciona severidad de alarma",
  1013 + "alarm-severity-required": "Se requiere especificar severidad de alarma.",
  1014 + "condition-duration": "Duración de condición",
  1015 + "condition-duration-value": "Valor de duración",
  1016 + "condition-duration-time-unit": "Unidad de tiempo",
  1017 + "condition-duration-value-range": "El valor debe estar en un rango desde 1 a 2147483647.",
  1018 + "condition-duration-value-pattern": "El valor de duración debe ser un número entero.",
  1019 + "condition-duration-value-required": "Se requiere valor de duración.",
  1020 + "condition-duration-time-unit-required": "Se requiere una unidad de tiempo.",
  1021 + "advanced-settings": "Ajustes avanzados",
  1022 + "alarm-rule-details": "Detalles",
  1023 + "add-alarm-rule-details": "Añadir detalles",
  1024 + "propagate-alarm": "Propagar alarma",
  1025 + "alarm-rule-relation-types-list": "Tipos de relación para propagar",
  1026 + "alarm-rule-relation-types-list-hint": "Si no está seleccionado 'propagar relaciones', las alarmas serán propagadas sin filtrar por relación.",
  1027 + "alarm-details": "Detalles de alarma",
  1028 + "alarm-rule-condition": "Condiciones de regla de alarma",
  1029 + "enter-alarm-rule-condition-prompt": "Por favor, añade una condición de alarma",
  1030 + "edit-alarm-rule-condition": "Editar condición de alarma",
  1031 + "device-provisioning": "Aprovisionamiento de dispositivos",
  1032 + "provision-strategy": "Estrategia de aprovisionamiento",
  1033 + "provision-strategy-required": "Se requiere estrategia de aprovisionamiento.",
  1034 + "provision-strategy-disabled": "Desactivado",
  1035 + "provision-strategy-created-new": "Permitir crear nuevos dispositivos",
  1036 + "provision-strategy-check-pre-provisioned": "Revisar dispositivos pre-aprovisionados",
  1037 + "provision-device-key": "Clave de aprovisionamiento",
  1038 + "provision-device-key-required": "Se requiere clave de aprovisionamiento.",
  1039 + "copy-provision-key": "Copiar clave de aprovisionamiento",
  1040 + "provision-key-copied-message": "La clave de aprovisionamiento se ha copiado al portapapeles",
  1041 + "provision-device-secret": "Secreto de aprovisionamiento",
  1042 + "provision-device-secret-required": "Se requiere secreto de aprovisionamiento.",
  1043 + "copy-provision-secret": "Copiar secreto de aprovisionamiento",
  1044 + "provision-secret-copied-message": "Se ha copiado el secreto de aprovisionamiento al portapapeles",
  1045 + "condition": "Condición",
  1046 + "condition-type": "Tipo de condición",
  1047 + "condition-type-simple": "Simple",
  1048 + "condition-type-duration": "Duración",
  1049 + "condition-during": "Durante {{during}}",
  1050 + "condition-type-repeating": "Repetitiva",
  1051 + "condition-type-required": "Se requiere tipo de condición.",
  1052 + "condition-repeating-value": "Nº de eventos",
  1053 + "condition-repeating-value-range": "El Nº de eventos debe estar en un rango de 1 to 2147483647.",
  1054 + "condition-repeating-value-pattern": "Nº de eventos debe ser un número entero.",
  1055 + "condition-repeating-value-required": "Se requiere Nº de eventos.",
  1056 + "condition-repeat-times": "Repetición { count, plural, 1 {1 vez} other {# veces} }",
  1057 + "schedule-type": "Tipo de horario",
  1058 + "schedule-type-required": "Tipo de horario requerido.",
  1059 + "schedule": "Horario",
  1060 + "edit-schedule": "Editar horario de alarma",
  1061 + "schedule-any-time": "Siempre activo",
  1062 + "schedule-specific-time": "Activo en una hora específica",
  1063 + "schedule-custom": "Personalizado",
  1064 + "schedule-day": {
  1065 + "monday": "Lunes",
  1066 + "tuesday": "Martes",
  1067 + "wednesday": "Miércoles",
  1068 + "thursday": "Jueves",
  1069 + "friday": "Viernes",
  1070 + "saturday": "Sábado",
  1071 + "sunday": "Domingo"
  1072 + },
  1073 + "schedule-days": "Días",
  1074 + "schedule-time": "Hora",
  1075 + "schedule-time-from": "De",
  1076 + "schedule-time-to": "Hasta",
  1077 + "schedule-days-of-week-required": "Debe ser seleccionado por lo menos un día de la semana."
712 1078 },
713   - "dialog": {
  1079 + "dialog": {
714 1080 "close": "Cerrar diálogo"
715 1081 },
716 1082 "direction": {
... ... @@ -766,6 +1132,10 @@
766 1132 "type-devices": "Dispositivos",
767 1133 "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista de # Dispositivos} }",
768 1134 "device-name-starts-with": "Dispositivos cuyos nombres comiencen por '{{prefix}}'",
  1135 + "type-device-profile": "Perfil de dispositivo",
  1136 + "type-device-profiles": "Perfiles de dispositivo",
  1137 + "list-of-device-profiles": "{ count, plural, 1 {un perfil} other {Lista de # perfiles} }",
  1138 + "device-profile-name-starts-with": "Perfiles cuyo nombre empiece por '{{prefix}}'",
769 1139 "type-asset": "Activo",
770 1140 "type-assets": "Activos",
771 1141 "list-of-assets": "{ count, plural, 1 {Un activo} other {Lista de # activos} }",
... ... @@ -785,7 +1155,11 @@
785 1155 "type-tenant": "Propietario",
786 1156 "type-tenants": "Propietarios",
787 1157 "list-of-tenants": "{ count, plural, 1 {Un propietario} other {Lista de # propietarios} }",
788   - "tenant-name-starts-with": "Tenants cuyos nombres comiencen por '{{prefix}}'",
  1158 + "tenant-name-starts-with": "Propietarios cuyo nombre comience por '{{prefix}}'",
  1159 + "type-tenant-profile": "Perfil de Propietario",
  1160 + "type-tenant-profiles": "Perfiles de propietario",
  1161 + "list-of-tenant-profiles": "{ count, plural, 1 {Un perfil de propietario} other {Lista de # perfiles de propietario} }",
  1162 + "tenant-profile-name-starts-with": "Pefiles de propietario cuyo nombre empiece por '{{prefix}}'",
789 1163 "type-customer": "Cliente",
790 1164 "type-customers": "Clientes",
791 1165 "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista de # clientes} }",
... ... @@ -812,6 +1186,8 @@
812 1186 "rulenode-name-starts-with": "Nodos de reglas cuyos nombres comienzan con '{{prefix}}'",
813 1187 "type-current-customer": "Cliente Actual",
814 1188 "type-current-tenant": "Propietario Actual",
  1189 + "type-current-user": "Usuario Actual",
  1190 + "type-current-user-owner": "Usuario Propietario Actual",
815 1191 "search": "Buscar entidades",
816 1192 "selected-entities": "{ count, plural, 1 {1 entidad} other {# entidades} } seleccionadas",
817 1193 "entity-label": "Etiqueta de entidad",
... ... @@ -819,10 +1195,11 @@
819 1195 "details": "Detalles de entidad",
820 1196 "no-entities-prompt": "No se han encontrado entidades",
821 1197 "no-data": "No hay datos que mostrar",
822   - "columns-to-display": "Columnas a Mostrar"
  1198 + "columns-to-display": "Columnas a Mostrar",
  1199 + "type-api-usage-state": "Estado de uso de la API"
823 1200 },
824 1201 "entity-field": {
825   - "created-time": "Tiempo de creación",
  1202 + "created-time": "Hora de creación",
826 1203 "name": "Nombre",
827 1204 "type": "Tipo",
828 1205 "first-name": "Nombre",
... ... @@ -855,6 +1232,7 @@
855 1232 "duplicate-alias-error": "Alias duplicado'{{alias}}'.<br>Los alias de Entity View deben ser únicos en el panel.",
856 1233 "configure-alias": "Configurar alias '{{alias}}'",
857 1234 "no-entity-views-matching": "No se encontraron vistas que coincidan con '{{entity}}'.",
  1235 + "public": "Público",
858 1236 "alias": "Alias",
859 1237 "alias-required": "Alias de vista de entidad es requerido.",
860 1238 "remove-alias": "Borrar alias de la vista de entidad",
... ... @@ -866,6 +1244,7 @@
866 1244 "entity-view-name-filter-required": "Nombre del filtro de vista de entidad es requerido.",
867 1245 "entity-view-name-filter-no-entity-view-matched": "No se encontraron vistas de entidad que comiencen con '{{entityView}}'.",
868 1246 "add": "Añadir vista de entidad",
  1247 + "entity-view-public": "Vista de entidad es pública",
869 1248 "assign-to-customer": "Asignar a cliente",
870 1249 "assign-entity-view-to-customer": "Asignar vista de entidad a cliente",
871 1250 "assign-entity-view-to-customer-text": "Por favor, seleccione las vistas de entidad para asignar al cliente",
... ... @@ -882,15 +1261,15 @@
882 1261 "unassign-entity-views-action-title": "Anular asignación { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } al cliente",
883 1262 "assign-new-entity-view": "Asignar nueva vista de entidad",
884 1263 "delete-entity-view-title": "¿Está seguro que quiere borrar la vista de entidad '{{entityViewName}}'?",
885   - "delete-entity-view-text": "¡Cuidado! Después de la confirmación, la vista de la entidad y todos los datos relacionados serán irrecuperables.",
  1264 + "delete-entity-view-text": "¡Cuidado! Tras la confirmación, la vista de la entidad y todos los datos relacionados serán irrecuperables.",
886 1265 "delete-entity-views-title": "¿Está seguro que quiere borrar las vistas de entidad { count, plural, 1 {1 entityView} other {# entityViews} }?",
887 1266 "delete-entity-views-action-title": "Borrar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }",
888   - "delete-entity-views-text": "¡Cuidado! Después de la confirmación, todas las vistas de entidades seleccionadas se eliminarán y todos los datos relacionados serán irrecuperables.",
  1267 + "delete-entity-views-text": "¡Cuidado! Tras la confirmación, todas las vistas de entidades seleccionadas se eliminarán y todos los datos relacionados serán irrecuperables.",
889 1268 "unassign-entity-view-title": "¿Está seguro que quiere anular la asignación de la vista de entidad '{{entityViewName}}'?",
890   - "unassign-entity-view-text": "Después de la confirmación, la vista de la entidad quedará sin asignar y el cliente no podrá acceder a ella.",
  1269 + "unassign-entity-view-text": "Tras la confirmación, la vista de la entidad quedará sin asignar y el cliente no podrá acceder a ella.",
891 1270 "unassign-entity-view": "Anular asignación de la vista de entidad",
892 1271 "unassign-entity-views-title": "¿Está seguro que quiere anular la asignación de { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?",
893   - "unassign-entity-views-text": "Después de la confirmación, todas las vistas de entidades seleccionadas quedarán sin asignar y el cliente no podrá acceder a ellas.",
  1272 + "unassign-entity-views-text": "Tras la confirmación, todas las vistas de entidades seleccionadas quedarán sin asignar y el cliente no podrá acceder a ellas.",
894 1273 "entity-view-type": "Tipo de vista de entidad",
895 1274 "entity-view-type-required": "Tipo de vista de entidad es requerido.",
896 1275 "select-entity-view-type": "Seleccione el tipo de vista de entidad",
... ... @@ -899,12 +1278,14 @@
899 1278 "no-entity-view-types-matching": "No se encontraron tipos de vista de entidad que coincidan con '{{entitySubtype}}'.",
900 1279 "entity-view-type-list-empty": "No hay tipos de vista de entidad seleccionados.",
901 1280 "entity-view-types": "Tipos de vista de entidad",
  1281 + "created-time": "Fecha de creación",
902 1282 "name": "Nombre",
903 1283 "name-required": "Nombre Requerido.",
904 1284 "description": "Descripción",
905 1285 "events": "Eventos",
906 1286 "details": "Detalles",
907 1287 "copyId": "Copiar el Id de la vista de entidad",
  1288 + "idCopiedMessage": "El Id de la vista de entidad se ha copiado al portapapeles",
908 1289 "assignedToCustomer": "Asignado a cliente",
909 1290 "unable-entity-view-device-alias-title": "No se puede eliminar el alias de vista de entidad",
910 1291 "unable-entity-view-device-alias-text": "El alias del dispositivo '{{entityViewAlias}}' no se puede borrar porque está siendo usado por el widget(s):<br/>{{widgetsList}}",
... ... @@ -930,9 +1311,11 @@
930 1311 "timeseries-data": "Datos de series temporales",
931 1312 "timeseries-data-hint": "Configure las claves de los datos de las series temporales de la entidad de destino que serán accesibles para la vista de la entidad. Los datos de esta serie temporal son de solo lectura.",
932 1313 "make-public-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea pública?",
933   - "make-public-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán públicos y accesibles para otros.",
  1314 + "make-public-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán públicos y accesibles para otros.",
934 1315 "make-private-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea privada?",
935   - "make-private-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros."
  1316 + "make-private-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros.",
  1317 + "search": "Buscar vistas de entidad",
  1318 + "selected-entity-views": "{ count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } seleccionadas"
936 1319 },
937 1320 "event": {
938 1321 "event-type": "Tipo de evento",
... ... @@ -965,7 +1348,7 @@
965 1348 },
966 1349 "extension": {
967 1350 "extensions": "Extensiones",
968   - "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } seleccionadas",
  1351 + "selected-extensions": "{ count, plural, 1 {1 extensión} other {# extensiones} } seleccionadas",
969 1352 "type": "Tipo",
970 1353 "key": "Clave",
971 1354 "value": "Valor",
... ... @@ -977,9 +1360,9 @@
977 1360 "delete": "Borrar Extensión",
978 1361 "add": "Añadir Extensión",
979 1362 "edit": "Editar Extensión",
980   - "delete-extension-title": "Estás seguro de borrar la extensión '{{extensionId}}'?",
  1363 + "delete-extension-title": "Eliminar la extensión '{{extensionId}}'?",
981 1364 "delete-extension-text": "Atención, tras la confirmación la extensión y sus datos serán borrados e irrecuperables.",
982   - "delete-extensions-title": "Estás seguro de borrar las extensiones { count, plural, 1 {1 extensión} other {# extensiones} }?",
  1365 + "delete-extensions-title": "Eliminar las extensiones { count, plural, 1 {1 extensión} other {# extensiones} }?",
983 1366 "delete-extensions-text": "Atención, tras la confirmación todas las extensiones seleccionadas y sus datos serán borrados e irrecuperables.",
984 1367 "converters": "Convertidores",
985 1368 "converter-id": "Id de convertidor",
... ... @@ -1120,6 +1503,93 @@
1120 1503 "file": "Fichero de extensiones",
1121 1504 "invalid-file-error": "Fichero de extensiones inválido"
1122 1505 },
  1506 + "filter": {
  1507 + "add": "Añadir filtro",
  1508 + "edit": "Editar filtro",
  1509 + "name": "Nombre de filtro",
  1510 + "name-required": "Se requiere nombre de filtro.",
  1511 + "duplicate-filter": "Ya existe un filtro con el mismo nombre.",
  1512 + "filters": "Filtros",
  1513 + "unable-delete-filter-title": "Error borrando filtro",
  1514 + "unable-delete-filter-text": "El filtro '{{filter}}' no puede ser borrado debido a que está siendo usado actualmente por los siguientes widgets:<br/>{{widgetsList}}",
  1515 + "duplicate-filter-error": "Se ha encontrado un filtro duplicado '{{filter}}'.<br>Los filtros deben ser únicos en el panel.",
  1516 + "missing-key-filters-error": "No se encontró la clave de filtros para el filtro '{{filter}}'.",
  1517 + "filter": "Filtro",
  1518 + "editable": "Editable",
  1519 + "no-filters-found": "No se encontraron filtros.",
  1520 + "no-filter-text": "No se ha especificado filtro",
  1521 + "add-filter-prompt": "Por favos, añadir filtro",
  1522 + "no-filter-matching": "'{{filter}}' no encontrado.",
  1523 + "create-new-filter": "Crear un filtro nuevo!",
  1524 + "filter-required": "Se requiere filtro.",
  1525 + "operation": {
  1526 + "operation": "Operación",
  1527 + "equal": "igual",
  1528 + "not-equal": "no igual",
  1529 + "starts-with": "comienza con",
  1530 + "ends-with": "acaba con",
  1531 + "contains": "contiene",
  1532 + "not-contains": "no contiene",
  1533 + "greater": "mayor que",
  1534 + "less": "menor que",
  1535 + "greater-or-equal": "mayor o igual",
  1536 + "less-or-equal": "menor o igual",
  1537 + "and": "y",
  1538 + "or": "o"
  1539 + },
  1540 + "ignore-case": "Ignorar mayús/minus",
  1541 + "value": "Valor",
  1542 + "remove-filter": "Borrar filtro",
  1543 + "preview": "Vista previa de filtro",
  1544 + "no-filters": "No hay filtros configurados",
  1545 + "add-filter": "Añadir filtro",
  1546 + "add-complex-filter": "Añadir filtro complejo",
  1547 + "add-complex": "Agregar filtro complejo",
  1548 + "complex-filter": "Filtro complejo",
  1549 + "edit-complex-filter": "Editar filtro complejo",
  1550 + "edit-filter-user-params": "Editar parámetros de usuario del filtro",
  1551 + "filter-user-params": "Filtro de parámetros de usuario (predicado)",
  1552 + "user-parameters": "Parámetros de usuario",
  1553 + "display-label": "Etiqueta a mostrar",
  1554 + "autogenerated-label": "Auto generar etiqueta",
  1555 + "order-priority": "Prioridad orden de campos",
  1556 + "key-filter": "Filtros (clave)",
  1557 + "key-filters": "Filtros (claves)",
  1558 + "key-name": "Nombre de clave",
  1559 + "key-name-required": "Se requiere nombre de clave.",
  1560 + "key-type": {
  1561 + "key-type": "Tipo de clave",
  1562 + "attribute": "Atributo",
  1563 + "timeseries": "Timeseries",
  1564 + "entity-field": "Campo de entidad"
  1565 + },
  1566 + "value-type": {
  1567 + "value-type": "Tipo de valor",
  1568 + "string": "Cadena",
  1569 + "numeric": "Numerico",
  1570 + "boolean": "Booleano",
  1571 + "date-time": "Fecha/Hora"
  1572 + },
  1573 + "value-type-required": "Se requiere tipo de valor.",
  1574 + "key-value-type-change-title": "Cambiar el tipo de valor de la clave?",
  1575 + "key-value-type-change-message": "Si confirmas el nuevo tipo, todos los filtros se borrarán.",
  1576 + "no-key-filters": "No hay filtros claves configurados",
  1577 + "add-key-filter": "Añadir filtro clave",
  1578 + "remove-key-filter": "Borrar filtro clave",
  1579 + "edit-key-filter": "Editar filtro clave",
  1580 + "date": "Fecha",
  1581 + "time": "Hora",
  1582 + "current-tenant": "Admin actual",
  1583 + "current-customer": "Cliente actual",
  1584 + "current-user": "Usuario actual",
  1585 + "current-device": "Dispositivo actual",
  1586 + "default-value": "Valor por defecto",
  1587 + "dynamic-source-type": "Tipo de origen dinámico",
  1588 + "no-dynamic-value": "Sin valor dinámico",
  1589 + "source-attribute": "Atributo de origen",
  1590 + "switch-to-dynamic-value": "Cambiar a valor dinámico",
  1591 + "switch-to-default-value": "Cambiar a valor por defecto"
  1592 + },
1123 1593 "fullscreen": {
1124 1594 "expand": "Expandir a Pantalla Completa",
1125 1595 "exit": "Salir de Pantalla Completa",
... ... @@ -1139,7 +1609,7 @@
1139 1609 "connector-type-required": "Se requiere tipo conector.",
1140 1610 "connectors": "Configuración de conectores",
1141 1611 "create-new-gateway": "Crear un gateway nuevo",
1142   - "create-new-gateway-text": "Estás seguro de crear un nuevo gateway con el nombre: '{{gatewayName}}'?",
  1612 + "create-new-gateway-text": "Crear un nuevo gateway con el nombre: '{{gatewayName}}'?",
1143 1613 "delete": "Borrar configuración",
1144 1614 "download-tip": "Descargar fichero de configuración",
1145 1615 "gateway": "Gateway",
... ... @@ -1201,17 +1671,12 @@
1201 1671 "tls-path-private-key": "Ruta a la clave privada en el gateway",
1202 1672 "toggle-fullscreen": "Pantalla completa fullscreen",
1203 1673 "transformer-json-config": "Configuración JSON*",
1204   - "update-config": "Añadir/actualizar configuración JSON",
1205   - "state-title": "Estado gateway",
1206   - "show-config-tip": "Mostrar configuración gateway",
1207   - "title-show-config": "Mostrar configuración gateway",
1208   - "read-only": "Solo lectura",
1209   - "read-write": ""
  1674 + "update-config": "Añadir/actualizar configuración JSON"
1210 1675 },
1211 1676 "grid": {
1212   - "delete-item-title": "¿Estás seguro que quieres eliminar este item?",
  1677 + "delete-item-title": "¿Quieres eliminar este item?",
1213 1678 "delete-item-text": "Atención, tras la confirmación el item será eliminado y la información relacionada será irrecuperable.",
1214   - "delete-items-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 item} other {# items} }?",
  1679 + "delete-items-title": "¿Quieres eliminar { count, plural, 1 {1 item} other {# items} }?",
1215 1680 "delete-items-action-title": "Eliminar { count, plural, 1 {1 item} other {# items} }",
1216 1681 "delete-items-text": "Atención, tras la confirmación los items seleccionados serán eliminados y la información relacionada será irrecuperable.",
1217 1682 "add-item-text": "Agregar nuevo item",
... ... @@ -1235,10 +1700,10 @@
1235 1700 "import": {
1236 1701 "no-file": "Ningún archivo seleccionado",
1237 1702 "drop-file": "Suelte un archivo JSON o haga clic para seleccionar un archivo para cargar.",
  1703 + "drop-file-csv": "Suelte un archivo CSV o haga clic para seleccionar un archivo para cargar.",
1238 1704 "column-value": "Valor",
1239 1705 "column-title": "Título",
1240 1706 "column-example": "Datos de ejemplo",
1241   - "drop-file-csv": "Suelte un archivo CSV o haga clic para seleccionar un archivo para cargar.",
1242 1707 "column-key": "Clave de atributo/telemetría",
1243 1708 "csv-delimiter": "Delimitador CSV",
1244 1709 "csv-first-line-header": "La primera línea contiene nombres de columna.",
... ... @@ -1257,6 +1722,7 @@
1257 1722 "entity-field": "Campo de entidad",
1258 1723 "access-token": "Token de acceso",
1259 1724 "isgateway": "Es Gateway",
  1725 + "activity-time-from-gateway-device": "Fecha de actividad desde el dispositivo gateway",
1260 1726 "description": "Descripción"
1261 1727 },
1262 1728 "stepper-text": {
... ... @@ -1300,6 +1766,7 @@
1300 1766 "legend": {
1301 1767 "direction": "Dirección de la leyenda",
1302 1768 "position": "Posición de la leyenda",
  1769 + "sort-legend": "Ordenar claves en leyenda",
1303 1770 "show-max": "Mostrar valor máximo",
1304 1771 "show-min": "Mostrar valor mínimo",
1305 1772 "show-avg": "Mostrar valor promedio",
... ... @@ -1376,20 +1843,21 @@
1376 1843 "any-relation-type": "Cualquier tipo",
1377 1844 "add": "Añadir relación",
1378 1845 "edit": "Editar relación",
1379   - "delete-to-relation-title": "¿Estás seguro que quieres eliminar la relación con la entidad '{{entityName}}'?",
  1846 + "delete-to-relation-title": "¿Quieres eliminar la relación con la entidad '{{entityName}}'?",
1380 1847 "delete-to-relation-text": "Atención, tras la confirmación la entidad '{{entityName}}' no estará relacionada con la entidad actual.",
1381   - "delete-to-relations-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?",
  1848 + "delete-to-relations-title": "¿Quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?",
1382 1849 "delete-to-relations-text": "Atención, tras la confirmación todas las relaciones seleccionadas se eliminarán y sus entidades correspondientes no estarán relacionadas con la entidad actual.",
1383   - "delete-from-relation-title": "¿Estás seguro que quieres eliminar la relación con la entidad '{{entityName}}'?",
  1850 + "delete-from-relation-title": "¿Quieres eliminar la relación con la entidad '{{entityName}}'?",
1384 1851 "delete-from-relation-text": "Atención, tras la confirmación la entidad actual no estará relacionada con la entidad '{{entityName}}'.",
1385   - "delete-from-relations-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?",
  1852 + "delete-from-relations-title": "¿Quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?",
1386 1853 "delete-from-relations-text": "Atención, tras la confirmación todas las relaciones seleccionadas se eliminarán y sus entidades correspondientes no estarán relacionadas con sus entidades correspondientes.",
1387 1854 "remove-relation-filter": "Quitar filtro de relación",
1388 1855 "add-relation-filter": "Añadir filtro de relación",
1389 1856 "any-relation": "Cualquier relación",
1390 1857 "relation-filters": "Filtro de relación",
1391 1858 "additional-info": "Información adicional (JSON)",
1392   - "invalid-additional-info": "Error al analizar el fichero JSON de información adicional."
  1859 + "invalid-additional-info": "Error al analizar el fichero JSON de información adicional.",
  1860 + "no-relations-text": "No se encontraron relaciones"
1393 1861 },
1394 1862 "rulechain": {
1395 1863 "rulechain": "Cadena de Regla",
... ... @@ -1401,9 +1869,9 @@
1401 1869 "description": "Descripción",
1402 1870 "add": "Añadir Cadena",
1403 1871 "set-root": "Hacer la cadena de reglas Raíz",
1404   - "set-root-rulechain-title": "¿Estás seguro de que desea hacer la cadena de reglas '{{ruleChainName}}' de tipo raíz?",
  1872 + "set-root-rulechain-title": "¿Desea hacer la cadena de reglas '{{ruleChainName}}' de tipo raíz?",
1405 1873 "set-root-rulechain-text": "Tras la confirmación, la cadena de reglas se volverá raíz y manejará todos los mensajes de transporte entrantes.",
1406   - "delete-rulechain-title": "¿Estás seguro que quieres eliminar la cadena de reglas '{{ruleChainName}}'?",
  1874 + "delete-rulechain-title": "¿Quieres eliminar la cadena de reglas '{{ruleChainName}}'?",
1407 1875 "delete-rulechain-text": "Atención, tras la confirmación la cadena de reglas y todos los datos serán irrecuperables.",
1408 1876 "delete-rulechains-title": "¿Está seguro que quieres eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }?",
1409 1877 "delete-rulechains-action-title": "Eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }",
... ... @@ -1426,7 +1894,10 @@
1426 1894 "no-rulechains-matching": "No se encontraron cadenas de reglas que coincidan con '{{entity}}' .",
1427 1895 "rulechain-required": "Cadena de reglas requerida",
1428 1896 "management": "Gestión de reglas",
1429   - "debug-mode": "Modo Debug"
  1897 + "debug-mode": "Modo Debug",
  1898 + "search": "Buscar cadenas de reglas",
  1899 + "selected-rulechains": "{ count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } seleccionadas",
  1900 + "open-rulechain": "Abrir cadena de reglas"
1430 1901 },
1431 1902 "rulenode": {
1432 1903 "details": "Detalles",
... ... @@ -1478,9 +1949,9 @@
1478 1949 "type-unknown": "Desconocido",
1479 1950 "type-unknown-details": "Regla de nodo no resuelta",
1480 1951 "directive-is-not-loaded": "La directiva de configuración definida '{{directiveName}}' no está disponible.",
1481   - "ui-resources-load-error": "Error al cargar los recursos de configuración ui.",
  1952 + "ui-resources-load-error": "Error al cargar los recursos de configuración UI.",
1482 1953 "invalid-target-rulechain": "No se puede resolver la cadena de reglas objetivo!",
1483   - "test-script-function": "Probar Script Función",
  1954 + "test-script-function": "Probar Script de función",
1484 1955 "message": "Mensaje",
1485 1956 "message-type": "Tipo de mensaje",
1486 1957 "select-message-type": "Seleccionar tipo de mensaje",
... ... @@ -1492,11 +1963,16 @@
1492 1963 "help": "Ayuda",
1493 1964 "reset-debug-mode": "Restablecer el modo de depuración en todos los nodos"
1494 1965 },
  1966 + "timezone": {
  1967 + "timezone": "Zona Horaria",
  1968 + "select-timezone": "Seleccionar zona horaria",
  1969 + "no-timezones-matching": "No hay zonas horarias que coincidan con '{{timezone}}'.",
  1970 + "timezone-required": "Se requiere zona horaria."
  1971 + },
1495 1972 "queue": {
1496   - "select_name": "Selecciona el nombre de la cola",
1497   - "name": "Nombre Cola",
1498   - "name_required": "Necesario especificar el nombre de cola"
1499   -
  1973 + "select_name": "Selecciona el nombre de la cola",
  1974 + "name": "Nombre Cola",
  1975 + "name_required": "Necesario especificar el nombre de cola"
1500 1976 },
1501 1977 "tenant": {
1502 1978 "tenant": "Propietario",
... ... @@ -1509,9 +1985,9 @@
1509 1985 "add-tenant-text": "Agregar nuevo propietario",
1510 1986 "no-tenants-text": "Ningún propietario encontrado",
1511 1987 "tenant-details": "Detalles del propietario",
1512   - "delete-tenant-title": "¿Estás seguro que quieres eliminar el propietario '{{tenantTitle}}'?",
  1988 + "delete-tenant-title": "¿Quieres eliminar el propietario '{{tenantTitle}}'?",
1513 1989 "delete-tenant-text": "Atención, tras la confirmación el propietario será eliminado y la información relacionada será irrecuperable.",
1514   - "delete-tenants-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 propietario} other {# propietarios} }?",
  1990 + "delete-tenants-title": "¿Quieres eliminar { count, plural, 1 {1 propietario} other {# propietarios} }?",
1515 1991 "delete-tenants-action-title": "Eliminar { count, plural, 1 {1 propietario} other {# propietarios} }",
1516 1992 "delete-tenants-text": "Atención, tras la confirmación los propietarios seleccionados serán eliminados y la información relacionada será irrecuperable.",
1517 1993 "title": "Título",
... ... @@ -1524,21 +2000,110 @@
1524 2000 "select-tenant": "Seleccionar propietario",
1525 2001 "no-tenants-matching": "No hay propietarios que coincidan con '{{entity}}' .",
1526 2002 "tenant-required": "Propietario requerido",
  2003 + "search": "Buscar propietarios",
  2004 + "selected-tenants": "{ count, plural, 1 {1 propietario} other {# propietarios} } seleccionados",
1527 2005 "isolated-tb-core": "Procesando en contenedor aislado",
1528 2006 "isolated-tb-rule-engine": "Procesando en contenedor Motor de Reglas aislado",
1529 2007 "isolated-tb-core-details": "Requiere microservicios separados por propietario aislado",
1530 2008 "isolated-tb-rule-engine-details": "Requiere microservicios separados por propietario aislado"
1531 2009 },
  2010 + "tenant-profile": {
  2011 + "tenant-profile": "Perfil de propietario",
  2012 + "tenant-profiles": "Perfiles de propietarios",
  2013 + "add": "Añadir perfil de propietario",
  2014 + "edit": "Editar perfil de propietario",
  2015 + "tenant-profile-details": "Detalles perfil de propietario",
  2016 + "no-tenant-profiles-text": "No se encontraron perfiles de propietario",
  2017 + "search": "Buscar perfiles de propietario",
  2018 + "selected-tenant-profiles": "{ count, plural, 1 {1 perfil de propietario} other {# perfiles de propietario} } seleccionados",
  2019 + "no-tenant-profiles-matching": "No se han encontrado perfiles de propietario que coincidan con '{{entity}}'.",
  2020 + "tenant-profile-required": "Se requiere perfil de propietario",
  2021 + "idCopiedMessage": "El ID de perfil de propietario se ha copiado al portapapeles",
  2022 + "set-default": "Hacer perfil propietario por defecto",
  2023 + "delete": "Borrar perfil",
  2024 + "copyId": "Copiar ID de perfil",
  2025 + "name": "Nombre",
  2026 + "name-required": "Se requiere nombre.",
  2027 + "data": "Datos de perfil",
  2028 + "profile-configuration": "Configuración de perfil",
  2029 + "description": "Descripción",
  2030 + "default": "Defecto",
  2031 + "delete-tenant-profile-title": "Eliminar el perfil propietario '{{tenantProfileName}}'?",
  2032 + "delete-tenant-profile-text": "Atención, tras la confirmación, el perfil de propietario será borrado y su información relacionada será irrecuperable.",
  2033 + "delete-tenant-profiles-title": "Eliminar { count, plural, 1 {1 perfil propietario} other {# perfiles propietarios} }?",
  2034 + "delete-tenant-profiles-text": "Atención, tras la confirmación, los perfiles seleccionados se eliminarán y su información relacionada será irrecuperable.",
  2035 + "set-default-tenant-profile-title": "Quieres hacer el perfil propietario '{{tenantProfileName}}' por defecto?",
  2036 + "set-default-tenant-profile-text": "Tras la confirmación, el perfil propietario será marcado por defecto y será usado por los nuevos perfiles propietarios que no tengan perfil específico.",
  2037 + "no-tenant-profiles-found": "No se encontraron perfiles de propietario.",
  2038 + "create-new-tenant-profile": "Crear un nuevo perfil!",
  2039 + "maximum-devices": "Nº Máximo de dispositivos (0 - sin límite)",
  2040 + "maximum-devices-required": "Nº Máximo de dispositivos requerido.",
  2041 + "maximum-devices-range": "Nº Máximo de dispositivos no puede ser negativo",
  2042 + "maximum-assets": "Nº Máximo de activos (0 - sin límite)",
  2043 + "maximum-assets-required": "Nº Máximo de activos requerido.",
  2044 + "maximum-assets-range": "Nº Máximo de activos no puede ser negativo",
  2045 + "maximum-customers": "Nº Máximo de clientes (0 - sin límite)",
  2046 + "maximum-customers-required": "Nº Máximo de clientes requerido.",
  2047 + "maximum-customers-range": "Nº Máximo de clientes no puede ser negativo",
  2048 + "maximum-users": "Nº Máximo de usuarios (0 - sin límite)",
  2049 + "maximum-users-required": "Nº Máximo de usuarios requerido.",
  2050 + "maximum-users-range": "Nº Máximo de usuarios no puede ser negativo",
  2051 + "maximum-dashboards": "Nº Máximo de paneles (0 - sin límite)",
  2052 + "maximum-dashboards-required": "Nº Máximo de paneles requerido.",
  2053 + "maximum-dashboards-range": "Nº Máximo de paneles no puede ser negativo",
  2054 + "maximum-rule-chains": "Nº Máximo de cadenas de reglas (0 - sin límite)",
  2055 + "maximum-rule-chains-required": "Nº Máximo de cadenas de reglas requerido.",
  2056 + "maximum-rule-chains-range": "Nº Máximo de cadenas de reglas no puede ser negativo",
  2057 + "transport-tenant-msg-rate-limit": "Tasa de mensajes de transporte por propietario.",
  2058 + "transport-tenant-telemetry-msg-rate-limit": "Tasa de mensajes de telemetría por propietario.",
  2059 + "transport-tenant-telemetry-data-points-rate-limit": "Tasa de datapoints por propietario.",
  2060 + "transport-device-msg-rate-limit": "Tasa de mensajes de dispositivo.",
  2061 + "transport-device-telemetry-msg-rate-limit": "Tasa de mensajes de telemetría de dispositivo.",
  2062 + "transport-device-telemetry-data-points-rate-limit": "Tasa de datapoints de telemetría de dispositivo.",
  2063 + "max-transport-messages": "Nº Máximo de mensajes de transporte (0 - sin límite)",
  2064 + "max-transport-messages-required": "Nº Máximo de mensajes de transporte requerido.",
  2065 + "max-transport-messages-range": "Nº Máximo de mensajes de transporte no puede ser negativo",
  2066 + "max-transport-data-points": "Nº Máximo de datapoints transporte (0 - sin límite)",
  2067 + "max-transport-data-points-required": "Nº Máximo de datapoints transporte requerido.",
  2068 + "max-transport-data-points-range": "Nº Máximo de datapoints transporte no puede ser negativo",
  2069 + "max-r-e-executions": "Nº Máximo de ejecuciones de motor de reglas (0 - sin límite)",
  2070 + "max-r-e-executions-required": "Nº Máximo de ejecuciones de motor de reglas requerido.",
  2071 + "max-r-e-executions-range": "Nº Máximo de ejecuciones de motor de reglas no puede ser negativo",
  2072 + "max-j-s-executions": "Nº Máximo de ejecuciones JavaScript (0 - sin límite)",
  2073 + "max-j-s-executions-required": "Nº Máximo de ejecuciones JavaScript requerido.",
  2074 + "max-j-s-executions-range": "Nº Máximo de ejecuciones JavaScript no puede ser negativo",
  2075 + "max-d-p-storage-days": "Nº Máximo de días a grabar en datapoints (0 - sin límite)",
  2076 + "max-d-p-storage-days-required": "Nº Máximo de días requerido.",
  2077 + "max-d-p-storage-days-range": "Nº Máximo de días no puede ser negativo",
  2078 + "default-storage-ttl-days": "Días por defecto grabado TTL (0 - sin límite)",
  2079 + "default-storage-ttl-days-required": "Días por defecto TTL requerido.",
  2080 + "default-storage-ttl-days-range": "Días por defecto TTL no puede ser negativo",
  2081 + "max-rule-node-executions-per-message": "Nº Máximo de ejecuciones (cadena de reglas) por mensaje (0 - sin límite)",
  2082 + "max-rule-node-executions-per-message-required": "Nº Máximo de ejecuciones por mensaje requerido.",
  2083 + "max-rule-node-executions-per-message-range": "Nº Máximo de ejecuciones por mensaje no puede ser negativo",
  2084 + "max-emails": "Nº Máximo de emails (0 - sin límite)",
  2085 + "max-emails-required": "Nº Máximo de emails requerido.",
  2086 + "max-emails-range": "Nº Máximo de emails no puede ser negativo",
  2087 + "max-sms": "Nº Máximo de mensajes SMS (0 - sin límite)",
  2088 + "max-sms-required": "Nº Máximo de mensajes SMS requerido.",
  2089 + "max-sms-range": "Nº Máximo de mensajes SMS no puede ser negativo"
  2090 + },
1532 2091 "timeinterval": {
1533   - "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segundos} }",
1534   - "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }",
1535   - "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }",
1536   - "days-interval": "{ days, plural, 1 {1 día} other {# días} }",
1537   - "days": "Días",
1538   - "hours": "Horas",
1539   - "minutes": "Minutos",
  2092 + "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segundos} }",
  2093 + "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }",
  2094 + "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }",
  2095 + "days-interval": "{ days, plural, 1 {1 día} other {# días} }",
  2096 + "days": "Días",
  2097 + "hours": "Horas",
  2098 + "minutes": "Minutos",
  2099 + "seconds": "Segundos",
  2100 + "advanced": "Avanzado"
  2101 + },
  2102 + "timeunit": {
1540 2103 "seconds": "Segundos",
1541   - "advanced": "Avanzado"
  2104 + "minutes": "Minutos",
  2105 + "hours": "Horas",
  2106 + "days": "Días"
1542 2107 },
1543 2108 "timewindow": {
1544 2109 "days": "{ days, plural, 1 { día } other {# días } }",
... ... @@ -1569,9 +2134,9 @@
1569 2134 "add-user-text": "Agregar nuevo usuario",
1570 2135 "no-users-text": "Ningún usuario encontrado",
1571 2136 "user-details": "Detalles del usuario",
1572   - "delete-user-title": "¿Estás seguro que quieres eliminar el usuario '{{userEmail}}'?",
  2137 + "delete-user-title": "¿Eliminar el usuario '{{userEmail}}'?",
1573 2138 "delete-user-text": "Atención, tras la confirmación el usuario seleccionado será eliminado y la información relacionada será irrecuperable.",
1574   - "delete-users-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 usuario} other {# usuarios} }?",
  2139 + "delete-users-title": "¿Eliminar { count, plural, 1 {1 usuario} other {# usuarios} }?",
1575 2140 "delete-users-action-title": "Borrar { count, plural, 1 {1 usuario} other {# usuarios} }",
1576 2141 "delete-users-text": "Atención, tras la confirmación los usuarios seleccionados serán eliminados y la información relacionada será irrecuperable.",
1577 2142 "activation-email-sent-message": "Mail de activación enviado con éxito!",
... ... @@ -1597,6 +2162,8 @@
1597 2162 "details": "Detalles",
1598 2163 "login-as-tenant-admin": "Iniciar sesión como Administrador Propietario",
1599 2164 "login-as-customer-user": "Iniciar sesión como Usuario Cliente",
  2165 + "search": "Buscar usuarios",
  2166 + "selected-users": "{ count, plural, 1 {1 usuario} other {# usuarios} } seleccionados",
1600 2167 "disable-account": "Deshabilitar cuenta de usuario",
1601 2168 "enable-account": "Habilitar cuenta de usuario",
1602 2169 "enable-account-message": "¡La cuenta de usuario se ha habilitado correctamente!",
... ... @@ -1606,18 +2173,23 @@
1606 2173 "type": "Tipo de valor",
1607 2174 "string": "Cadena de texto",
1608 2175 "string-value": "Valor de cadena de texto",
  2176 + "string-value-required": "Se requiere valor de cadena de texto",
1609 2177 "integer": "Nro entero",
1610 2178 "integer-value": "Valor de nro entero",
1611   - "invalid-integer-value": "Valor inválido",
  2179 + "integer-value-required": "Se requiere valor entero",
  2180 + "invalid-integer-value": "Valor de entero inválido",
1612 2181 "double": "Nro decimal",
1613 2182 "double-value": "Valor nro decimal",
  2183 + "double-value-required": "Se requiere valor nro decimal",
1614 2184 "boolean": "Booleano",
1615 2185 "boolean-value": "Valor booleano",
1616 2186 "false": "Falso",
1617 2187 "true": "Verdadero",
1618 2188 "long": "Nro Largo",
1619 2189 "json": "JSON",
1620   - "json-value": "Valor JSON"
  2190 + "json-value": "Valor JSON",
  2191 + "json-value-invalid": "El valor JSON tiene un formato inválido",
  2192 + "json-value-required": "Se requiere valor JSON"
1621 2193 },
1622 2194 "widget": {
1623 2195 "widget-library": "Bibloteca de Widgets",
... ... @@ -1629,7 +2201,7 @@
1629 2201 "widget-type-load-error": "El widget no pudo ser cargado debido a estos errores:",
1630 2202 "remove": "Eliminar widget",
1631 2203 "edit": "Editar widget",
1632   - "remove-widget-title": "¿Estás seguro que quieres eliminar el widget '{{widgetTitle}}'?",
  2204 + "remove-widget-title": "¿Eliminar el widget '{{widgetTitle}}'?",
1633 2205 "remove-widget-text": "Atención, tras la confirmación el widget será eliminado y toda la información relacionada será irrecuperable..",
1634 2206 "timeseries": "Series de tiempo",
1635 2207 "search-data": "Buscar datos",
... ... @@ -1653,6 +2225,7 @@
1653 2225 "type": "Tipo",
1654 2226 "resources": "Recursos",
1655 2227 "resource-url": "URL JavaScript/CSS",
  2228 + "resource-is-module": "Es módulo",
1656 2229 "remove-resource": "Eliminar recurso",
1657 2230 "add-resource": "Agregar recurso",
1658 2231 "html": "HTML",
... ... @@ -1662,7 +2235,7 @@
1662 2235 "datakey-settings-schema": "Esquema de configuración de clave de datos",
1663 2236 "javascript": "Javascript",
1664 2237 "js": "JS",
1665   - "remove-widget-type-title": "¿Estás seguro que quieres eliminar el tipo del widget '{{widgetName}}'?",
  2238 + "remove-widget-type-title": "¿Eliminar el tipo del widget '{{widgetName}}'?",
1666 2239 "remove-widget-type-text": "Atención, tras la confirmación el tipo será eliminado y la información relacionada será irrecuperable.",
1667 2240 "remove-widget-type": "Eliminar tipo de widget.",
1668 2241 "add-widget-type": "Agregar nuevo tipo de widget",
... ... @@ -1670,7 +2243,10 @@
1670 2243 "widget-template-load-failed-error": "Error al cargar la plantilla del widget!",
1671 2244 "add": "Agregar Widget",
1672 2245 "undo": "Deshacer cambios",
1673   - "export": "Exportar widget"
  2246 + "export": "Exportar widget",
  2247 + "no-data": "No hay datos para mostrar en widget",
  2248 + "data-overflow": "El widget muestra {{count}} de {{total}} entidades",
  2249 + "alarm-data-overflow": "El widget muestra alarmas para {{allowedEntities}} entidades (máximo permitido) de {{totalEntities}} entidades"
1674 2250 },
1675 2251 "widget-action": {
1676 2252 "header-button": "Botón de encabezado widget",
... ... @@ -1683,7 +2259,14 @@
1683 2259 "target-dashboard-state-required": "Se requiere estado de panel de destino",
1684 2260 "set-entity-from-widget": "Establecer entidad desde widget",
1685 2261 "target-dashboard": "Panel de destino",
1686   - "open-right-layout": "Abrir diseño de panel (derecho)(vista móvil)"
  2262 + "open-right-layout": "Abrir diseño de panel (derecho)(vista móvil)",
  2263 + "open-in-separate-dialog": "Abrir en un diálogo separado",
  2264 + "dialog-title": "Título del diálogo",
  2265 + "dialog-hide-dashboard-toolbar": "Ocultar barra de herramientas en el diálogo",
  2266 + "dialog-width": "Ancho de diálogo en porcentaje relativo al ancho del viewport",
  2267 + "dialog-height": "Alto de diálogo en porcentaje relativo al alto del viewport",
  2268 + "dialog-size-range-error": "El tamaño del diálogo debe ser entre un rango de 1 a 100",
  2269 + "open-new-browser-tab": "Abrir en una nueva pestaña"
1687 2270 },
1688 2271 "widgets-bundle": {
1689 2272 "current": "Paquete actual",
... ... @@ -1697,9 +2280,9 @@
1697 2280 "empty": "Paquete de widgets vacío.",
1698 2281 "details": "Detalles",
1699 2282 "widgets-bundle-details": "Detalles del paquete de Widgets",
1700   - "delete-widgets-bundle-title": "¿Estás seguro que quieres eliminar el paquete de widgets '{{widgetsBundleTitle}}'?",
  2283 + "delete-widgets-bundle-title": "¿Eliminar el paquete de widgets '{{widgetsBundleTitle}}'?",
1701 2284 "delete-widgets-bundle-text": "Atención, tras la confirmación todos los paquetes seleccionados serán eliminados y su información relacionada será irrecuperable.",
1702   - "delete-widgets-bundles-title": "¿Estás seguro que deseas eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }?",
  2285 + "delete-widgets-bundles-title": "¿Eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }?",
1703 2286 "delete-widgets-bundles-action-title": "Eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }",
1704 2287 "delete-widgets-bundles-text": "Atención, tras la confirmación todos los paquetes seleccionados serán eliminados y la información relacionada será irrecuperable.",
1705 2288 "no-widgets-bundles-matching": "Ningún paquete '{{widgetsBundle}}' encontrado.",
... ... @@ -1710,7 +2293,10 @@
1710 2293 "export-failed-error": "Imposible exportar paquete de widgets: {{error}}",
1711 2294 "create-new-widgets-bundle": "Crear nuevo paquete de widgets",
1712 2295 "widgets-bundle-file": "Archivo de paquete de widgets",
1713   - "invalid-widgets-bundle-file-error": "Imposible importar paquete de widgets: Estructura de datos inválida."
  2296 + "invalid-widgets-bundle-file-error": "Imposible importar paquete de widgets: Estructura de datos inválida.",
  2297 + "search": "Buscar paquete de widgets",
  2298 + "selected-widgets-bundles": "{ count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} } seleccionados",
  2299 + "open-widgets-bundle": "Abrir paquete de widgets"
1714 2300 },
1715 2301 "widget-config": {
1716 2302 "data": "Datos",
... ... @@ -1749,6 +2335,7 @@
1749 2335 "action": "Acción",
1750 2336 "add-action": "Añadir acción",
1751 2337 "search-actions": "Buscar acciones",
  2338 + "no-actions-text": "No se encontraron actiones",
1752 2339 "action-source": "Origen de acción",
1753 2340 "action-source-required": "Origen de acción requerido.",
1754 2341 "action-name": "Nombre",
... ... @@ -1760,7 +2347,7 @@
1760 2347 "edit-action": "Editar acción",
1761 2348 "delete-action": "Borrar acción",
1762 2349 "delete-action-title": "Borrar acción de widget",
1763   - "delete-action-text": "Estás seguro de borrar la acción de widget con el nombre '{{actionName}}'?",
  2350 + "delete-action-text": "Eliminar la acción de widget con el nombre '{{actionName}}'?",
1764 2351 "display-icon": "Mostrar icono del título",
1765 2352 "icon-color": "Color del icono",
1766 2353 "icon-size": "Tamaño del icono"
... ... @@ -1828,7 +2415,7 @@
1828 2415 "Custom interval": "Intervalo personalizado",
1829 2416 "Interval": "Intervalo",
1830 2417 "Step size": "Número de pasos",
1831   - "Ok": "De acuerdo"
  2418 + "Ok": "Ok"
1832 2419 }
1833 2420 },
1834 2421 "input-widgets": {
... ... @@ -1846,8 +2433,11 @@
1846 2433 "entity-coordinate-required": "Se requieren ambos campos (latitud y longitud)",
1847 2434 "entity-timeseries-required": "Se requiere la serie de tiempo de la entidad",
1848 2435 "get-location": "Obtener localización actual",
  2436 + "invalid-date": "Fecha inválida",
1849 2437 "latitude": "Latitud",
1850 2438 "longitude": "Longitud",
  2439 + "min-value-error": "El valor mínimo es {{value}}",
  2440 + "max-value-error": "El valor máximo es {{value}}",
1851 2441 "not-allowed-entity": "La entidad seleccionada no puede tener atributos compartidos",
1852 2442 "no-attribute-selected": "No se seleccionó ningún atributo",
1853 2443 "no-datakey-selected": "No se seleccionó ninguna clave de datos",
... ... @@ -1856,6 +2446,9 @@
1856 2446 "no-image": "Sin imagen",
1857 2447 "no-support-geolocation": "Tu navegador no soporta geolocalización",
1858 2448 "no-support-web-camera": "No hay cámara web compatible",
  2449 + "enable-https-use-widget": "Por favor, activa HTTPS para poder usar este widget",
  2450 + "no-found-your-camera": "No es posible encontrar la cámara",
  2451 + "no-permission-camera": "Permiso denegado por el usuario / Esta página no tiene permisos para usar la cámara",
1859 2452 "no-timeseries-selected": "No hay series de tiempo seleccionadas",
1860 2453 "secret-key": "Clave",
1861 2454 "secret-key-required": "Clave requerida",
... ...
... ... @@ -1118,6 +1118,13 @@
1118 1118 dependencies:
1119 1119 regenerator-runtime "^0.13.4"
1120 1120
  1121 +"@babel/runtime@^7.12.5":
  1122 + version "7.12.5"
  1123 + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
  1124 + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
  1125 + dependencies:
  1126 + regenerator-runtime "^0.13.4"
  1127 +
1121 1128 "@babel/template@7.10.4", "@babel/template@^7.10.4":
1122 1129 version "7.10.4"
1123 1130 resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
... ... @@ -7941,7 +7948,7 @@ rc-util@^4.15.3:
7941 7948 react-lifecycles-compat "^3.0.4"
7942 7949 shallowequal "^1.1.0"
7943 7950
7944   -rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.3.0:
  7951 +rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.3.0:
7945 7952 version "5.4.0"
7946 7953 resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004"
7947 7954 integrity sha512-kXDn1JyLJTAWLBFt+fjkTcUtXhxKkipQCobQmxIEVrX62iXgo24z8YKoWehWfMxPZFPE+RXqrmEu9j5kHz/Lrg==
... ... @@ -7949,6 +7956,15 @@ rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.3.0:
7949 7956 react-is "^16.12.0"
7950 7957 shallowequal "^1.1.0"
7951 7958
  7959 +rc-util@^5.0.6:
  7960 + version "5.7.0"
  7961 + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.7.0.tgz#776b14cf5bbfc24f419fd40c42ffadddda0718fc"
  7962 + integrity sha512-0hh5XkJ+vBDeMJsHElqT1ijMx+gC3gpClwQ10h/5hccrrgrMx8VUem183KLlH1YrWCfMMPmDXWWNnwsn+p6URw==
  7963 + dependencies:
  7964 + "@babel/runtime" "^7.12.5"
  7965 + react-is "^16.12.0"
  7966 + shallowequal "^1.1.0"
  7967 +
7952 7968 rc-virtual-list@^1.1.2:
7953 7969 version "1.1.6"
7954 7970 resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.6.tgz#b255baf9aacde149a8893324e6307214094f4c0a"
... ...