Commit 3ea5314495c857796198a9183e2a160562a2f87f

Authored by Igor Kulikov
1 parent 84647a07

Home dashboard feature. Enable tooltips for flot chars in mobile mode. Disable w…

…idgets interaction in widget library and when adding widget to dashboard.
Showing 51 changed files with 1182 additions and 84 deletions
@@ -26,22 +26,6 @@ @@ -26,22 +26,6 @@
26 } 26 }
27 }, 27 },
28 { 28 {
29 - "alias": "basic_timeseries",  
30 - "name": "Timeseries - Flot",  
31 - "descriptor": {  
32 - "type": "timeseries",  
33 - "sizeX": 8,  
34 - "sizeY": 5,  
35 - "resources": [],  
36 - "templateHtml": "",  
37 - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",  
38 - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",  
39 - "settingsSchema": "{}",  
40 - "dataKeySettingsSchema": "{}",  
41 - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"  
42 - }  
43 - },  
44 - {  
45 "alias": "doughnut_chart_js", 29 "alias": "doughnut_chart_js",
46 "name": "Doughnut - Chart.js", 30 "name": "Doughnut - Chart.js",
47 "descriptor": { 31 "descriptor": {
@@ -71,7 +55,7 @@ @@ -71,7 +55,7 @@
71 "resources": [], 55 "resources": [],
72 "templateHtml": "", 56 "templateHtml": "",
73 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n", 57 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n",
74 - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", 58 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n",
75 "settingsSchema": "{}\n", 59 "settingsSchema": "{}\n",
76 "dataKeySettingsSchema": "{}\n", 60 "dataKeySettingsSchema": "{}\n",
77 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" 61 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
@@ -138,8 +122,8 @@ @@ -138,8 +122,8 @@
138 } 122 }
139 }, 123 },
140 { 124 {
141 - "alias": "timeseries_bars_flot",  
142 - "name": "Timeseries Bars - Flot", 125 + "alias": "state_chart",
  126 + "name": "State Chart",
143 "descriptor": { 127 "descriptor": {
144 "type": "timeseries", 128 "type": "timeseries",
145 "sizeX": 8, 129 "sizeX": 8,
@@ -147,15 +131,15 @@ @@ -147,15 +131,15 @@
147 "resources": [], 131 "resources": [],
148 "templateHtml": "", 132 "templateHtml": "",
149 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", 133 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
150 - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", 134 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
151 "settingsSchema": "{}", 135 "settingsSchema": "{}",
152 "dataKeySettingsSchema": "{}", 136 "dataKeySettingsSchema": "{}",
153 - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" 137 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
154 } 138 }
155 }, 139 },
156 { 140 {
157 - "alias": "state_chart",  
158 - "name": "State Chart", 141 + "alias": "basic_timeseries",
  142 + "name": "Timeseries - Flot",
159 "descriptor": { 143 "descriptor": {
160 "type": "timeseries", 144 "type": "timeseries",
161 "sizeX": 8, 145 "sizeX": 8,
@@ -163,11 +147,27 @@ @@ -163,11 +147,27 @@
163 "resources": [], 147 "resources": [],
164 "templateHtml": "", 148 "templateHtml": "",
165 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", 149 "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
166 - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", 150 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
167 "settingsSchema": "{}", 151 "settingsSchema": "{}",
168 "dataKeySettingsSchema": "{}", 152 "dataKeySettingsSchema": "{}",
169 - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" 153 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
  154 + }
  155 + },
  156 + {
  157 + "alias": "timeseries_bars_flot",
  158 + "name": "Timeseries Bars - Flot",
  159 + "descriptor": {
  160 + "type": "timeseries",
  161 + "sizeX": 8,
  162 + "sizeY": 5,
  163 + "resources": [],
  164 + "templateHtml": "",
  165 + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
  166 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  167 + "settingsSchema": "{}",
  168 + "dataKeySettingsSchema": "{}",
  169 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
170 } 170 }
171 } 171 }
172 ] 172 ]
173 -} 173 +}
  1 +{
  2 + "widgetsBundle": {
  3 + "alias": "navigation_widgets",
  4 + "title": "Navigation widgets",
  5 + "image": null
  6 + },
  7 + "widgetTypes": [
  8 + {
  9 + "alias": "navigation_cards",
  10 + "name": "Navigation cards",
  11 + "descriptor": {
  12 + "type": "static",
  13 + "sizeX": 7,
  14 + "sizeY": 6,
  15 + "resources": [],
  16 + "templateHtml": "<tb-navigation-cards-widget [ctx]=\"ctx\"></tb-navigation-cards-widget>",
  17 + "templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}",
  18 + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n",
  19 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"filterType\": {\n \"title\": \"Filter type\",\n \"type\": \"string\",\n \"default\": \"all\"\n },\n \"filter\": {\n \"title\": \"Items\",\n \"type\": \"array\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"filterType\",\n \"type\": \"radios\",\n \"direction\": \"row\",\n \"titleMap\": [\n {\n \"value\": \"all\",\n \"name\": \"All items\"\n },\n {\n \"value\": \"include\",\n \"name\": \"Include items\"\n },\n {\n \"value\": \"exclude\",\n \"name\": \"Exclude items\"\n }\n ]\n },\n {\n \"key\": \"filter\",\n \"type\": \"rc-select\",\n \"condition\": \"model.filterType !== 'all'\",\n \"tags\": true,\n \"placeholder\": \"Enter urls to filter\",\n \"items\": [{\"value\": \"/devices\", \"label\": \"/devices\"}, {\"value\": \"/assets\", \"label\": \"/assets\"}, {\"value\": \"/deviceProfies\", \"label\": \"/deviceProfies\"}]\n }\n ]\n}\n",
  20 + "dataKeySettingsSchema": "{}\n",
  21 + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
  22 + }
  23 + },
  24 + {
  25 + "alias": "navigation_card",
  26 + "name": "Navigation card",
  27 + "descriptor": {
  28 + "type": "static",
  29 + "sizeX": 2.5,
  30 + "sizeY": 2,
  31 + "resources": [],
  32 + "templateHtml": "<tb-navigation-card-widget [ctx]=\"ctx\"></tb-navigation-card-widget>",
  33 + "templateCss": "",
  34 + "controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n",
  35 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"name\": {\n \"title\": \"Title\",\n \"type\": \"string\",\n \"default\": \"{i18n:device.devices}\"\n },\n \"icon\": {\n \"title\": \"icon\",\n \"type\": \"string\",\n \"default\": \"devices_other\"\n },\n \"path\": {\n \"title\": \"Navigation path\",\n \"type\": \"string\",\n \"default\": \"/devices\"\n }\n },\n \"required\": [\"name\", \"icon\", \"path\"]\n },\n \"form\": [\n \"name\",\n {\n \"key\": \"icon\",\n \"type\": \"icon\"\n },\n \"path\"\n ]\n}\n",
  36 + "dataKeySettingsSchema": "{}\n",
  37 + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
  38 + }
  39 + }
  40 + ]
  41 +}
@@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 import org.springframework.beans.factory.annotation.Value; 20 import org.springframework.beans.factory.annotation.Value;
19 import org.springframework.http.HttpStatus; 21 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 22 import org.springframework.security.access.prepost.PreAuthorize;
@@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer; @@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer;
30 import org.thingsboard.server.common.data.Dashboard; 32 import org.thingsboard.server.common.data.Dashboard;
31 import org.thingsboard.server.common.data.DashboardInfo; 33 import org.thingsboard.server.common.data.DashboardInfo;
32 import org.thingsboard.server.common.data.EntityType; 34 import org.thingsboard.server.common.data.EntityType;
  35 +import org.thingsboard.server.common.data.HomeDashboard;
  36 +import org.thingsboard.server.common.data.HomeDashboardInfo;
33 import org.thingsboard.server.common.data.ShortCustomerInfo; 37 import org.thingsboard.server.common.data.ShortCustomerInfo;
  38 +import org.thingsboard.server.common.data.Tenant;
  39 +import org.thingsboard.server.common.data.User;
34 import org.thingsboard.server.common.data.audit.ActionType; 40 import org.thingsboard.server.common.data.audit.ActionType;
35 import org.thingsboard.server.common.data.exception.ThingsboardException; 41 import org.thingsboard.server.common.data.exception.ThingsboardException;
36 import org.thingsboard.server.common.data.id.CustomerId; 42 import org.thingsboard.server.common.data.id.CustomerId;
@@ -39,7 +45,9 @@ import org.thingsboard.server.common.data.id.TenantId; @@ -39,7 +45,9 @@ import org.thingsboard.server.common.data.id.TenantId;
39 import org.thingsboard.server.common.data.page.PageData; 45 import org.thingsboard.server.common.data.page.PageData;
40 import org.thingsboard.server.common.data.page.PageLink; 46 import org.thingsboard.server.common.data.page.PageLink;
41 import org.thingsboard.server.common.data.page.TimePageLink; 47 import org.thingsboard.server.common.data.page.TimePageLink;
  48 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
42 import org.thingsboard.server.queue.util.TbCoreComponent; 49 import org.thingsboard.server.queue.util.TbCoreComponent;
  50 +import org.thingsboard.server.service.security.model.SecurityUser;
43 import org.thingsboard.server.service.security.permission.Operation; 51 import org.thingsboard.server.service.security.permission.Operation;
44 import org.thingsboard.server.service.security.permission.Resource; 52 import org.thingsboard.server.service.security.permission.Resource;
45 53
@@ -53,6 +61,9 @@ public class DashboardController extends BaseController { @@ -53,6 +61,9 @@ public class DashboardController extends BaseController {
53 61
54 public static final String DASHBOARD_ID = "dashboardId"; 62 public static final String DASHBOARD_ID = "dashboardId";
55 63
  64 + private static final String HOME_DASHBOARD_ID = "homeDashboardId";
  65 + private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
  66 +
56 @Value("${dashboard.max_datapoints_limit}") 67 @Value("${dashboard.max_datapoints_limit}")
57 private long maxDatapointsLimit; 68 private long maxDatapointsLimit;
58 69
@@ -472,4 +483,100 @@ public class DashboardController extends BaseController { @@ -472,4 +483,100 @@ public class DashboardController extends BaseController {
472 throw handleException(e); 483 throw handleException(e);
473 } 484 }
474 } 485 }
  486 +
  487 + @PreAuthorize("isAuthenticated()")
  488 + @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET)
  489 + @ResponseBody
  490 + public HomeDashboard getHomeDashboard() throws ThingsboardException {
  491 + try {
  492 + SecurityUser securityUser = getCurrentUser();
  493 + if (securityUser.isSystemAdmin()) {
  494 + return null;
  495 + }
  496 + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
  497 + JsonNode additionalInfo = user.getAdditionalInfo();
  498 + HomeDashboard homeDashboard;
  499 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  500 + if (homeDashboard == null) {
  501 + if (securityUser.isCustomerUser()) {
  502 + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId());
  503 + additionalInfo = customer.getAdditionalInfo();
  504 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  505 + }
  506 + if (homeDashboard == null) {
  507 + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId());
  508 + additionalInfo = tenant.getAdditionalInfo();
  509 + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
  510 + }
  511 + }
  512 + return homeDashboard;
  513 + } catch (Exception e) {
  514 + throw handleException(e);
  515 + }
  516 + }
  517 +
  518 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  519 + @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET)
  520 + @ResponseBody
  521 + public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException {
  522 + try {
  523 + Tenant tenant = tenantService.findTenantById(getTenantId());
  524 + JsonNode additionalInfo = tenant.getAdditionalInfo();
  525 + DashboardId dashboardId = null;
  526 + boolean hideDashboardToolbar = true;
  527 + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID)) {
  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)) {
  570 + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
  571 + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
  572 + Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
  573 + boolean hideDashboardToolbar = true;
  574 + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
  575 + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
  576 + }
  577 + return new HomeDashboard(dashboard, hideDashboardToolbar);
  578 + }
  579 + } catch (Exception e) {}
  580 + return null;
  581 + }
475 } 582 }
@@ -185,6 +185,8 @@ public class ThingsboardInstallService { @@ -185,6 +185,8 @@ public class ThingsboardInstallService {
185 case "3.2.0": 185 case "3.2.0":
186 log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ..."); 186 log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
187 databaseEntitiesUpgradeService.upgradeDatabase("3.2.0"); 187 databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
  188 + case "3.2.1":
  189 + log.info("Upgrading ThingsBoard from version 3.2.1 to 3.3.0 ...");
188 log.info("Updating system data..."); 190 log.info("Updating system data...");
189 systemDataLoaderService.updateSystemWidgets(); 191 systemDataLoaderService.updateSystemWidgets();
190 break; 192 break;
@@ -438,6 +438,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @@ -438,6 +438,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
438 this.deleteSystemWidgetBundle("input_widgets"); 438 this.deleteSystemWidgetBundle("input_widgets");
439 this.deleteSystemWidgetBundle("date"); 439 this.deleteSystemWidgetBundle("date");
440 this.deleteSystemWidgetBundle("entity_admin_widgets"); 440 this.deleteSystemWidgetBundle("entity_admin_widgets");
  441 + this.deleteSystemWidgetBundle("navigation_widgets");
441 installScripts.loadSystemWidgets(); 442 installScripts.loadSystemWidgets();
442 } 443 }
443 444
  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 +}
@@ -53,8 +53,8 @@ export class AliasController implements IAliasController { @@ -53,8 +53,8 @@ export class AliasController implements IAliasController {
53 private stateControllerHolder: StateControllerHolder, 53 private stateControllerHolder: StateControllerHolder,
54 private origEntityAliases: EntityAliases, 54 private origEntityAliases: EntityAliases,
55 private origFilters: Filters) { 55 private origFilters: Filters) {
56 - this.entityAliases = deepClone(this.origEntityAliases);  
57 - this.filters = deepClone(this.origFilters); 56 + this.entityAliases = deepClone(this.origEntityAliases) || {};
  57 + this.filters = deepClone(this.origFilters) || {};
58 this.userFilters = {}; 58 this.userFilters = {};
59 } 59 }
60 60
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 /// 15 ///
16 16
17 import { Injectable, NgZone } from '@angular/core'; 17 import { Injectable, NgZone } from '@angular/core';
18 -import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'; 18 +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
19 import { AuthService } from '../auth/auth.service'; 19 import { AuthService } from '../auth/auth.service';
20 import { select, Store } from '@ngrx/store'; 20 import { select, Store } from '@ngrx/store';
21 import { AppState } from '../core.state'; 21 import { AppState } from '../core.state';
@@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum'; @@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum';
28 import { DialogService } from '@core/services/dialog.service'; 28 import { DialogService } from '@core/services/dialog.service';
29 import { TranslateService } from '@ngx-translate/core'; 29 import { TranslateService } from '@ngx-translate/core';
30 import { UtilsService } from '@core/services/utils.service'; 30 import { UtilsService } from '@core/services/utils.service';
  31 +import { isObject } from '@core/utils';
31 32
32 @Injectable({ 33 @Injectable({
33 providedIn: 'root' 34 providedIn: 'root'
@@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service'; @@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
35 export class AuthGuard implements CanActivate, CanActivateChild { 36 export class AuthGuard implements CanActivate, CanActivateChild {
36 37
37 constructor(private store: Store<AppState>, 38 constructor(private store: Store<AppState>,
  39 + private router: Router,
38 private authService: AuthService, 40 private authService: AuthService,
39 private dialogService: DialogService, 41 private dialogService: DialogService,
40 private utils: UtilsService, 42 private utils: UtilsService,
@@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild {
115 if (data.auth && data.auth.indexOf(authority) === -1) { 117 if (data.auth && data.auth.indexOf(authority) === -1) {
116 this.dialogService.forbidden(); 118 this.dialogService.forbidden();
117 return of(false); 119 return of(false);
  120 + } else if (data.redirectTo) {
  121 + let redirect;
  122 + if (isObject(data.redirectTo)) {
  123 + redirect = data.redirectTo[authority];
  124 + } else {
  125 + redirect = data.redirectTo;
  126 + }
  127 + return of(this.router.parseUrl(redirect));
118 } else { 128 } else {
119 return of(true); 129 return of(true);
120 } 130 }
@@ -20,7 +20,7 @@ import { Observable } from 'rxjs'; @@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
20 import { HttpClient } from '@angular/common/http'; 20 import { HttpClient } from '@angular/common/http';
21 import { PageLink } from '@shared/models/page/page-link'; 21 import { PageLink } from '@shared/models/page/page-link';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
23 -import { Dashboard, DashboardInfo } from '@shared/models/dashboard.models'; 23 +import { Dashboard, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models';
24 import { WINDOW } from '@core/services/window.service'; 24 import { WINDOW } from '@core/services/window.service';
25 import { NavigationEnd, Router } from '@angular/router'; 25 import { NavigationEnd, Router } from '@angular/router';
26 import { filter, map, publishReplay, refCount } from 'rxjs/operators'; 26 import { filter, map, publishReplay, refCount } from 'rxjs/operators';
@@ -122,6 +122,19 @@ export class DashboardService { @@ -122,6 +122,19 @@ export class DashboardService {
122 defaultHttpOptionsFromConfig(config)); 122 defaultHttpOptionsFromConfig(config));
123 } 123 }
124 124
  125 + public getHomeDashboard(config?: RequestConfig): Observable<HomeDashboard> {
  126 + return this.http.get<HomeDashboard>('/api/dashboard/home', defaultHttpOptionsFromConfig(config));
  127 + }
  128 +
  129 + public getTenantHomeDashboardInfo(config?: RequestConfig): Observable<HomeDashboardInfo> {
  130 + return this.http.get<HomeDashboardInfo>('/api/tenant/dashboard/home/info', defaultHttpOptionsFromConfig(config));
  131 + }
  132 +
  133 + public setTenantHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo, config?: RequestConfig): Observable<any> {
  134 + return this.http.post<any>('/api/tenant/dashboard/home/info', homeDashboardInfo,
  135 + defaultHttpOptionsFromConfig(config));
  136 + }
  137 +
125 public getPublicDashboardLink(dashboard: DashboardInfo): string | null { 138 public getPublicDashboardLink(dashboard: DashboardInfo): string | null {
126 if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) { 139 if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) {
127 const publicCustomers = dashboard.assignedCustomers 140 const publicCustomers = dashboard.assignedCustomers
@@ -284,6 +284,13 @@ export class MenuService { @@ -284,6 +284,13 @@ export class MenuService {
284 }, 284 },
285 { 285 {
286 id: guid(), 286 id: guid(),
  287 + name: 'admin.home-settings',
  288 + type: 'link',
  289 + path: '/settings/home',
  290 + icon: 'settings_applications'
  291 + },
  292 + {
  293 + id: guid(),
287 name: 'audit-log.audit-logs', 294 name: 'audit-log.audit-logs',
288 type: 'link', 295 type: 'link',
289 path: '/auditLogs', 296 path: '/auditLogs',
@@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
23 [widgetLayouts]="{}" 23 [widgetLayouts]="{}"
24 [isEdit]="false" 24 [isEdit]="false"
25 [isMobile]="true" 25 [isMobile]="true"
  26 + [disableWidgetInteraction]="true"
26 [isEditActionEnabled]="false" 27 [isEditActionEnabled]="false"
27 [isExportActionEnabled]="false" 28 [isExportActionEnabled]="false"
28 [isRemoveActionEnabled]="false" 29 [isRemoveActionEnabled]="false"
@@ -34,6 +35,7 @@ @@ -34,6 +35,7 @@
34 [widgetLayouts]="{}" 35 [widgetLayouts]="{}"
35 [isEdit]="false" 36 [isEdit]="false"
36 [isMobile]="true" 37 [isMobile]="true"
  38 + [disableWidgetInteraction]="true"
37 [isEditActionEnabled]="false" 39 [isEditActionEnabled]="false"
38 [isExportActionEnabled]="false" 40 [isExportActionEnabled]="false"
39 [isRemoveActionEnabled]="false" 41 [isRemoveActionEnabled]="false"
@@ -45,6 +47,7 @@ @@ -45,6 +47,7 @@
45 [widgetLayouts]="{}" 47 [widgetLayouts]="{}"
46 [isEdit]="false" 48 [isEdit]="false"
47 [isMobile]="true" 49 [isMobile]="true"
  50 + [disableWidgetInteraction]="true"
48 [isEditActionEnabled]="false" 51 [isEditActionEnabled]="false"
49 [isExportActionEnabled]="false" 52 [isExportActionEnabled]="false"
50 [isRemoveActionEnabled]="false" 53 [isRemoveActionEnabled]="false"
@@ -56,6 +59,7 @@ @@ -56,6 +59,7 @@
56 [widgetLayouts]="{}" 59 [widgetLayouts]="{}"
57 [isEdit]="false" 60 [isEdit]="false"
58 [isMobile]="true" 61 [isMobile]="true"
  62 + [disableWidgetInteraction]="true"
59 [isEditActionEnabled]="false" 63 [isEditActionEnabled]="false"
60 [isExportActionEnabled]="false" 64 [isExportActionEnabled]="false"
61 [isRemoveActionEnabled]="false" 65 [isRemoveActionEnabled]="false"
@@ -67,6 +71,7 @@ @@ -67,6 +71,7 @@
67 [widgetLayouts]="{}" 71 [widgetLayouts]="{}"
68 [isEdit]="false" 72 [isEdit]="false"
69 [isMobile]="true" 73 [isMobile]="true"
  74 + [disableWidgetInteraction]="true"
70 [isEditActionEnabled]="false" 75 [isEditActionEnabled]="false"
71 [isExportActionEnabled]="false" 76 [isExportActionEnabled]="false"
72 [isRemoveActionEnabled]="false" 77 [isRemoveActionEnabled]="false"
@@ -53,6 +53,7 @@ @@ -53,6 +53,7 @@
53 [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight" 53 [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
54 [isMobile]="isMobile" 54 [isMobile]="isMobile"
55 [isMobileDisabled]="widgetEditMode" 55 [isMobileDisabled]="widgetEditMode"
  56 + [disableWidgetInteraction]="isEdit"
56 [isEditActionEnabled]="isEdit" 57 [isEditActionEnabled]="isEdit"
57 [isExportActionEnabled]="isEdit && !widgetEditMode" 58 [isExportActionEnabled]="isEdit && !widgetEditMode"
58 [isRemoveActionEnabled]="isEdit && !widgetEditMode" 59 [isRemoveActionEnabled]="isEdit && !widgetEditMode"
@@ -150,7 +150,7 @@ @@ -150,7 +150,7 @@
150 </button> 150 </button>
151 </div> 151 </div>
152 </div> 152 </div>
153 - <div fxFlex fxLayout="column" class="tb-widget-content"> 153 + <div fxFlex fxLayout="column" class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">
154 <tb-widget fxFlex 154 <tb-widget fxFlex
155 #widgetComponent 155 #widgetComponent
156 [dashboardWidget]="widget" 156 [dashboardWidget]="widget"
@@ -128,6 +128,9 @@ div.tb-widget { @@ -128,6 +128,9 @@ div.tb-widget {
128 } 128 }
129 129
130 .tb-widget-content { 130 .tb-widget-content {
  131 + &.tb-no-interaction {
  132 + pointer-events: none;
  133 + }
131 tb-widget { 134 tb-widget {
132 position: relative; 135 position: relative;
133 width: 100%; 136 width: 100%;
@@ -114,6 +114,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -114,6 +114,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
114 isRemoveActionEnabled: boolean; 114 isRemoveActionEnabled: boolean;
115 115
116 @Input() 116 @Input()
  117 + disableWidgetInteraction = false;
  118 +
  119 + @Input()
117 dashboardStyle: {[klass: string]: any}; 120 dashboardStyle: {[klass: string]: any};
118 121
119 @Input() 122 @Input()
@@ -175,7 +175,7 @@ export class TbFlot { @@ -175,7 +175,7 @@ export class TbFlot {
175 autoHighlight: this.tooltipIndividual === true, 175 autoHighlight: this.tooltipIndividual === true,
176 markings: [] 176 markings: []
177 }, 177 },
178 - selection : { mode : ctx.isMobile ? null : 'x' }, 178 + selection : { mode : 'x' },
179 legend : { 179 legend : {
180 show: false 180 show: false
181 } 181 }
@@ -702,7 +702,7 @@ export class TbFlot { @@ -702,7 +702,7 @@ export class TbFlot {
702 } 702 }
703 703
704 public checkMouseEvents() { 704 public checkMouseEvents() {
705 - const enabled = !this.ctx.isMobile && !this.ctx.isEdit; 705 + const enabled = !this.ctx.isEdit;
706 if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) { 706 if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) {
707 this.mouseEventsEnabled = enabled; 707 this.mouseEventsEnabled = enabled;
708 if (this.$element) { 708 if (this.$element) {
  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 +}
@@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen @@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen
35 import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; 35 import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component';
36 import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; 36 import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
37 import { ImportExportService } from '@home/components/import-export/import-export.service'; 37 import { ImportExportService } from '@home/components/import-export/import-export.service';
  38 +import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
  39 +import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
38 40
39 @NgModule({ 41 @NgModule({
40 declarations: 42 declarations:
@@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor @@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
50 MultipleInputWidgetComponent, 52 MultipleInputWidgetComponent,
51 TripAnimationComponent, 53 TripAnimationComponent,
52 PhotoCameraInputWidgetComponent, 54 PhotoCameraInputWidgetComponent,
53 - GatewayFormComponent 55 + GatewayFormComponent,
  56 + NavigationCardsWidgetComponent,
  57 + NavigationCardWidgetComponent
54 ], 58 ],
55 imports: [ 59 imports: [
56 CommonModule, 60 CommonModule,
@@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor @@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
68 MultipleInputWidgetComponent, 72 MultipleInputWidgetComponent,
69 TripAnimationComponent, 73 TripAnimationComponent,
70 PhotoCameraInputWidgetComponent, 74 PhotoCameraInputWidgetComponent,
71 - GatewayFormComponent 75 + GatewayFormComponent,
  76 + NavigationCardsWidgetComponent,
  77 + NavigationCardWidgetComponent
72 ], 78 ],
73 providers: [ 79 providers: [
74 CustomDialogService, 80 CustomDialogService,
@@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
32 import { OAuth2Service } from '@core/http/oauth2.service'; 32 import { OAuth2Service } from '@core/http/oauth2.service';
33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module'; 33 import { UserProfileResolver } from '@home/pages/profile/profile-routing.module';
34 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; 34 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
  35 +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
35 36
36 @Injectable() 37 @Injectable()
37 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { 38 export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@@ -48,7 +49,7 @@ const routes: Routes = [ @@ -48,7 +49,7 @@ const routes: Routes = [
48 { 49 {
49 path: 'settings', 50 path: 'settings',
50 data: { 51 data: {
51 - auth: [Authority.SYS_ADMIN], 52 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
52 breadcrumb: { 53 breadcrumb: {
53 label: 'admin.system-settings', 54 label: 'admin.system-settings',
54 icon: 'settings' 55 icon: 'settings'
@@ -57,8 +58,13 @@ const routes: Routes = [ @@ -57,8 +58,13 @@ const routes: Routes = [
57 children: [ 58 children: [
58 { 59 {
59 path: '', 60 path: '',
60 - redirectTo: 'general',  
61 - pathMatch: 'full' 61 + data: {
  62 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  63 + redirectTo: {
  64 + SYS_ADMIN: '/settings/general',
  65 + TENANT_ADMIN: '/settings/home'
  66 + }
  67 + }
62 }, 68 },
63 { 69 {
64 path: 'general', 70 path: 'general',
@@ -127,6 +133,19 @@ const routes: Routes = [ @@ -127,6 +133,19 @@ const routes: Routes = [
127 resolve: { 133 resolve: {
128 loginProcessingUrl: OAuth2LoginProcessingUrlResolver 134 loginProcessingUrl: OAuth2LoginProcessingUrlResolver
129 } 135 }
  136 + },
  137 + {
  138 + path: 'home',
  139 + component: HomeSettingsComponent,
  140 + canDeactivate: [ConfirmOnExitGuard],
  141 + data: {
  142 + auth: [Authority.TENANT_ADMIN],
  143 + title: 'admin.home-settings',
  144 + breadcrumb: {
  145 + label: 'admin.home-settings',
  146 + icon: 'settings_applications'
  147 + }
  148 + }
130 } 149 }
131 ] 150 ]
132 } 151 }
@@ -26,6 +26,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m @@ -26,6 +26,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m
26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; 26 import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
27 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; 27 import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
28 import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; 28 import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
  29 +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
29 30
30 @NgModule({ 31 @NgModule({
31 declarations: 32 declarations:
@@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial @@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial
35 SmsProviderComponent, 36 SmsProviderComponent,
36 SendTestSmsDialogComponent, 37 SendTestSmsDialogComponent,
37 SecuritySettingsComponent, 38 SecuritySettingsComponent,
38 - OAuth2SettingsComponent 39 + OAuth2SettingsComponent,
  40 + HomeSettingsComponent
39 ], 41 ],
40 imports: [ 42 imports: [
41 CommonModule, 43 CommonModule,
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.home-settings</span>
  23 + </div>
  24 + </mat-card-title>
  25 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  26 + </mat-progress-bar>
  27 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  28 + <mat-card-content style="padding-top: 16px;">
  29 + <form [formGroup]="homeSettings" (ngSubmit)="save()">
  30 + <fieldset [disabled]="isLoading$ | async">
  31 + <section class="tb-default-dashboard" fxFlex fxLayout="column">
  32 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  33 + <tb-dashboard-autocomplete
  34 + fxFlex
  35 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  36 + formControlName="dashboardId"
  37 + [dashboardsScope]="'tenant'"
  38 + [selectFirstDashboard]="false"
  39 + ></tb-dashboard-autocomplete>
  40 + <mat-checkbox fxFlex formControlName="hideDashboardToolbar">
  41 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  42 + </mat-checkbox>
  43 + </section>
  44 + </section>
  45 + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
  46 + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || homeSettings.invalid || !homeSettings.dirty"
  47 + type="submit">{{'action.save' | translate}}
  48 + </button>
  49 + </div>
  50 + </fieldset>
  51 + </form>
  52 + </mat-card-content>
  53 + </mat-card>
  54 +</div>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@import "../../../../../scss/constants";
  18 +
  19 +:host {
  20 + .tb-default-dashboard {
  21 + tb-dashboard-autocomplete {
  22 + @media #{$mat-gt-sm} {
  23 + padding-right: 12px;
  24 + }
  25 +
  26 + @media #{$mat-lt-md} {
  27 + padding-bottom: 12px;
  28 + }
  29 + }
  30 + mat-checkbox {
  31 + @media #{$mat-gt-sm} {
  32 + margin-top: 16px;
  33 + }
  34 + }
  35 + }
  36 +}
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { Router } from '@angular/router';
  22 +import { FormBuilder, FormGroup } from '@angular/forms';
  23 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  24 +import { DashboardService } from '@core/http/dashboard.service';
  25 +import { HomeDashboardInfo } from '@shared/models/dashboard.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
  27 +import { DashboardId } from '@shared/models/id/dashboard-id';
  28 +
  29 +@Component({
  30 + selector: 'tb-home-settings',
  31 + templateUrl: './home-settings.component.html',
  32 + styleUrls: ['./home-settings.component.scss', './settings-card.scss']
  33 +})
  34 +export class HomeSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
  35 +
  36 + homeSettings: FormGroup;
  37 +
  38 + constructor(protected store: Store<AppState>,
  39 + private router: Router,
  40 + private dashboardService: DashboardService,
  41 + public fb: FormBuilder) {
  42 + super(store);
  43 + }
  44 +
  45 + ngOnInit() {
  46 + this.homeSettings = this.fb.group({
  47 + dashboardId: [null],
  48 + hideDashboardToolbar: [true]
  49 + });
  50 + this.dashboardService.getTenantHomeDashboardInfo().subscribe(
  51 + (homeDashboardInfo) => {
  52 + this.setHomeDashboardInfo(homeDashboardInfo);
  53 + }
  54 + );
  55 + }
  56 +
  57 + save(): void {
  58 + const strDashboardId = this.homeSettings.get('dashboardId').value;
  59 + const dashboardId: DashboardId = strDashboardId ? new DashboardId(strDashboardId) : null;
  60 + const hideDashboardToolbar = this.homeSettings.get('hideDashboardToolbar').value;
  61 + const homeDashboardInfo: HomeDashboardInfo = {
  62 + dashboardId,
  63 + hideDashboardToolbar
  64 + };
  65 + this.dashboardService.setTenantHomeDashboardInfo(homeDashboardInfo).subscribe(
  66 + () => {
  67 + this.setHomeDashboardInfo(homeDashboardInfo);
  68 + }
  69 + );
  70 + }
  71 +
  72 + confirmForm(): FormGroup {
  73 + return this.homeSettings;
  74 + }
  75 +
  76 + private setHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo) {
  77 + this.homeSettings.reset({
  78 + dashboardId: homeDashboardInfo?.dashboardId?.id,
  79 + hideDashboardToolbar: isDefinedAndNotNull(homeDashboardInfo?.hideDashboardToolbar) ?
  80 + homeDashboardInfo?.hideDashboardToolbar : true
  81 + });
  82 + }
  83 +
  84 +}
@@ -72,6 +72,21 @@ @@ -72,6 +72,21 @@
72 <mat-label translate>customer.description</mat-label> 72 <mat-label translate>customer.description</mat-label>
73 <textarea matInput formControlName="description" rows="2"></textarea> 73 <textarea matInput formControlName="description" rows="2"></textarea>
74 </mat-form-field> 74 </mat-form-field>
  75 + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
  76 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  77 + <tb-dashboard-autocomplete
  78 + fxFlex
  79 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  80 + formControlName="homeDashboardId"
  81 + [dashboardsScope]="'customer'"
  82 + [customerId]="entity?.id.id"
  83 + [selectFirstDashboard]="false"
  84 + ></tb-dashboard-autocomplete>
  85 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  86 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  87 + </mat-checkbox>
  88 + </section>
  89 + </section>
75 </div> 90 </div>
76 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> 91 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
77 </fieldset> 92 </fieldset>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + .tb-default-dashboard {
  20 + tb-dashboard-autocomplete {
  21 + @media #{$mat-gt-sm} {
  22 + padding-right: 12px;
  23 + }
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding-bottom: 12px;
  27 + }
  28 + }
  29 + mat-checkbox {
  30 + @media #{$mat-gt-sm} {
  31 + margin-top: 16px;
  32 + }
  33 + }
  34 + }
  35 +}
@@ -23,10 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti @@ -23,10 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
23 import { TranslateService } from '@ngx-translate/core'; 23 import { TranslateService } from '@ngx-translate/core';
24 import { ContactBasedComponent } from '../../components/entity/contact-based.component'; 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 @Component({ 28 @Component({
28 selector: 'tb-customer', 29 selector: 'tb-customer',
29 - templateUrl: './customer.component.html' 30 + templateUrl: './customer.component.html',
  31 + styleUrls: ['./customer.component.scss']
30 }) 32 })
31 export class CustomerComponent extends ContactBasedComponent<Customer> { 33 export class CustomerComponent extends ContactBasedComponent<Customer> {
32 34
@@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { @@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
54 title: [entity ? entity.title : '', [Validators.required]], 56 title: [entity ? entity.title : '', [Validators.required]],
55 additionalInfo: this.fb.group( 57 additionalInfo: this.fb.group(
56 { 58 {
57 - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] 59 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  60 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  61 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  62 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
58 } 63 }
59 ) 64 )
60 } 65 }
@@ -65,6 +70,11 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { @@ -65,6 +70,11 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
65 this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic; 70 this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic;
66 this.entityForm.patchValue({title: entity.title}); 71 this.entityForm.patchValue({title: entity.title});
67 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); 72 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  73 + this.entityForm.patchValue({additionalInfo:
  74 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  75 + this.entityForm.patchValue({additionalInfo:
  76 + {homeDashboardHideToolbar: entity.additionalInfo &&
  77 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
68 } 78 }
69 79
70 onCustomerIdCopied(event) { 80 onCustomerIdCopied(event) {
@@ -14,11 +14,25 @@ @@ -14,11 +14,25 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { NgModule } from '@angular/core';  
18 -import { RouterModule, Routes } from '@angular/router'; 17 +import { Injectable, NgModule } from '@angular/core';
  18 +import { Resolve, RouterModule, Routes } from '@angular/router';
19 19
20 import { HomeLinksComponent } from './home-links.component'; 20 import { HomeLinksComponent } from './home-links.component';
21 import { Authority } from '@shared/models/authority.enum'; 21 import { Authority } from '@shared/models/authority.enum';
  22 +import { Observable } from 'rxjs';
  23 +import { HomeDashboard } from '@shared/models/dashboard.models';
  24 +import { DashboardService } from '@core/http/dashboard.service';
  25 +
  26 +@Injectable()
  27 +export class HomeDashboardResolver implements Resolve<HomeDashboard> {
  28 +
  29 + constructor(private dashboardService: DashboardService) {
  30 + }
  31 +
  32 + resolve(): Observable<HomeDashboard> {
  33 + return this.dashboardService.getHomeDashboard();
  34 + }
  35 +}
22 36
23 const routes: Routes = [ 37 const routes: Routes = [
24 { 38 {
@@ -31,12 +45,18 @@ const routes: Routes = [ @@ -31,12 +45,18 @@ const routes: Routes = [
31 label: 'home.home', 45 label: 'home.home',
32 icon: 'home' 46 icon: 'home'
33 } 47 }
  48 + },
  49 + resolve: {
  50 + homeDashboard: HomeDashboardResolver
34 } 51 }
35 } 52 }
36 ]; 53 ];
37 54
38 @NgModule({ 55 @NgModule({
39 imports: [RouterModule.forChild(routes)], 56 imports: [RouterModule.forChild(routes)],
40 - exports: [RouterModule] 57 + exports: [RouterModule],
  58 + providers: [
  59 + HomeDashboardResolver
  60 + ]
41 }) 61 })
42 export class HomeLinksRoutingModule { } 62 export class HomeLinksRoutingModule { }
@@ -15,23 +15,26 @@ @@ -15,23 +15,26 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">  
19 - <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">  
20 - <mat-card style="width: 100%;">  
21 - <mat-card-title>  
22 - <span translate class="mat-headline">{{section.name}}</span>  
23 - </mat-card-title>  
24 - <mat-card-content>  
25 - <mat-grid-list rowHeight="170px" [cols]="section.places.length">  
26 - <mat-grid-tile *ngFor="let place of section.places">  
27 - <a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">  
28 - <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>  
29 - <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>  
30 - <span translate>{{place.name}}</span>  
31 - </a>  
32 - </mat-grid-tile>  
33 - </mat-grid-list>  
34 - </mat-card-content>  
35 - </mat-card>  
36 - </mat-grid-tile>  
37 -</mat-grid-list> 18 +<tb-dashboard-page *ngIf="homeDashboard; else homeLinks" [embedded]="true" [dashboard]="homeDashboard" [hideToolbar]="homeDashboard.hideDashboardToolbar"></tb-dashboard-page>
  19 +<ng-template #homeLinks>
  20 + <mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">
  21 + <mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">
  22 + <mat-card style="width: 100%;">
  23 + <mat-card-title>
  24 + <span translate class="mat-headline">{{section.name}}</span>
  25 + </mat-card-title>
  26 + <mat-card-content>
  27 + <mat-grid-list rowHeight="170px" [cols]="section.places.length">
  28 + <mat-grid-tile *ngFor="let place of section.places">
  29 + <a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
  30 + <mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
  31 + <mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
  32 + <span translate>{{place.name}}</span>
  33 + </a>
  34 + </mat-grid-tile>
  35 + </mat-grid-list>
  36 + </mat-card-content>
  37 + </mat-card>
  38 + </mat-grid-tile>
  39 + </mat-grid-list>
  40 +</ng-template>
@@ -15,6 +15,11 @@ @@ -15,6 +15,11 @@
15 */ 15 */
16 @import '../../../../../scss/constants'; 16 @import '../../../../../scss/constants';
17 17
  18 +:host {
  19 + width: 100%;
  20 + height: 100%;
  21 +}
  22 +
18 :host ::ng-deep { 23 :host ::ng-deep {
19 .tb-home-links { 24 .tb-home-links {
20 .mat-headline { 25 .mat-headline {
@@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service'; @@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service';
19 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; 19 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
20 import { MediaBreakpoints } from '@shared/models/constants'; 20 import { MediaBreakpoints } from '@shared/models/constants';
21 import { HomeSection } from '@core/services/menu.models'; 21 import { HomeSection } from '@core/services/menu.models';
  22 +import { ActivatedRoute } from '@angular/router';
  23 +import { HomeDashboard } from '@shared/models/dashboard.models';
22 24
23 @Component({ 25 @Component({
24 selector: 'tb-home-links', 26 selector: 'tb-home-links',
@@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit { @@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit {
31 33
32 cols = 2; 34 cols = 2;
33 35
  36 + homeDashboard: HomeDashboard = this.route.snapshot.data.homeDashboard;
  37 +
34 constructor(private menuService: MenuService, 38 constructor(private menuService: MenuService,
35 - public breakpointObserver: BreakpointObserver) { 39 + public breakpointObserver: BreakpointObserver,
  40 + private route: ActivatedRoute) {
36 } 41 }
37 42
38 ngOnInit() { 43 ngOnInit() {
39 - this.updateColumnCount();  
40 - this.breakpointObserver  
41 - .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])  
42 - .subscribe((state: BreakpointState) => this.updateColumnCount()); 44 + if (!this.homeDashboard) {
  45 + this.updateColumnCount();
  46 + this.breakpointObserver
  47 + .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])
  48 + .subscribe((state: BreakpointState) => this.updateColumnCount());
  49 + }
43 } 50 }
44 51
45 private updateColumnCount() { 52 private updateColumnCount() {
@@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common'; @@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common';
20 import { HomeLinksRoutingModule } from './home-links-routing.module'; 20 import { HomeLinksRoutingModule } from './home-links-routing.module';
21 import { HomeLinksComponent } from './home-links.component'; 21 import { HomeLinksComponent } from './home-links.component';
22 import { SharedModule } from '@app/shared/shared.module'; 22 import { SharedModule } from '@app/shared/shared.module';
  23 +import { HomeComponentsModule } from '@home/components/home-components.module';
23 24
24 @NgModule({ 25 @NgModule({
25 declarations: 26 declarations:
@@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module'; @@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module';
29 imports: [ 30 imports: [
30 CommonModule, 31 CommonModule,
31 SharedModule, 32 SharedModule,
  33 + HomeComponentsModule,
32 HomeLinksRoutingModule 34 HomeLinksRoutingModule
33 ] 35 ]
34 }) 36 })
@@ -63,6 +63,20 @@ @@ -63,6 +63,20 @@
63 </mat-option> 63 </mat-option>
64 </mat-select> 64 </mat-select>
65 </mat-form-field> 65 </mat-form-field>
  66 + <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row">
  67 + <tb-dashboard-autocomplete
  68 + fxFlex
  69 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  70 + formControlName="homeDashboardId"
  71 + [dashboardsScope]="user?.authority === authorities.TENANT_ADMIN ? 'tenant' : 'customer'"
  72 + [tenantId]="user?.tenantId?.id"
  73 + [customerId]="user?.customerId?.id"
  74 + [selectFirstDashboard]="false"
  75 + ></tb-dashboard-autocomplete>
  76 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  77 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  78 + </mat-checkbox>
  79 + </section>
66 <div fxLayout="row" style="padding-bottom: 16px;"> 80 <div fxLayout="row" style="padding-bottom: 16px;">
67 <button mat-button mat-raised-button color="primary" 81 <button mat-button mat-raised-button color="primary"
68 type="button" 82 type="button"
@@ -38,5 +38,21 @@ @@ -38,5 +38,21 @@
38 font-size: 16px; 38 font-size: 16px;
39 font-weight: 400; 39 font-weight: 400;
40 } 40 }
  41 + .tb-home-dashboard {
  42 + tb-dashboard-autocomplete {
  43 + @media #{$mat-gt-sm} {
  44 + padding-right: 12px;
  45 + }
  46 +
  47 + @media #{$mat-lt-md} {
  48 + padding-bottom: 12px;
  49 + }
  50 + }
  51 + mat-checkbox {
  52 + @media #{$mat-gt-sm} {
  53 + margin-top: 16px;
  54 + }
  55 + }
  56 + }
41 } 57 }
42 } 58 }
@@ -32,6 +32,7 @@ import { MatDialog } from '@angular/material/dialog'; @@ -32,6 +32,7 @@ import { MatDialog } from '@angular/material/dialog';
32 import { DialogService } from '@core/services/dialog.service'; 32 import { DialogService } from '@core/services/dialog.service';
33 import { AuthService } from '@core/auth/auth.service'; 33 import { AuthService } from '@core/auth/auth.service';
34 import { ActivatedRoute } from '@angular/router'; 34 import { ActivatedRoute } from '@angular/router';
  35 +import { isDefinedAndNotNull } from '@core/utils';
35 36
36 @Component({ 37 @Component({
37 selector: 'tb-profile', 38 selector: 'tb-profile',
@@ -66,7 +67,9 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -66,7 +67,9 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
66 email: ['', [Validators.required, Validators.email]], 67 email: ['', [Validators.required, Validators.email]],
67 firstName: [''], 68 firstName: [''],
68 lastName: [''], 69 lastName: [''],
69 - language: [''] 70 + language: [''],
  71 + homeDashboardId: [null],
  72 + homeDashboardHideToolbar: [true]
70 }); 73 });
71 } 74 }
72 75
@@ -76,6 +79,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -76,6 +79,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
76 this.user.additionalInfo = {}; 79 this.user.additionalInfo = {};
77 } 80 }
78 this.user.additionalInfo.lang = this.profile.get('language').value; 81 this.user.additionalInfo.lang = this.profile.get('language').value;
  82 + this.user.additionalInfo.homeDashboardId = this.profile.get('homeDashboardId').value;
  83 + this.user.additionalInfo.homeDashboardHideToolbar = this.profile.get('homeDashboardHideToolbar').value;
79 this.userService.saveUser(this.user).subscribe( 84 this.userService.saveUser(this.user).subscribe(
80 (user) => { 85 (user) => {
81 this.userLoaded(user); 86 this.userLoaded(user);
@@ -106,12 +111,23 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -106,12 +111,23 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
106 this.user = user; 111 this.user = user;
107 this.profile.reset(user); 112 this.profile.reset(user);
108 let lang; 113 let lang;
109 - if (user.additionalInfo && user.additionalInfo.lang) {  
110 - lang = user.additionalInfo.lang;  
111 - } else { 114 + let homeDashboardId;
  115 + let homeDashboardHideToolbar = true;
  116 + if (user.additionalInfo) {
  117 + if (user.additionalInfo.lang) {
  118 + lang = user.additionalInfo.lang;
  119 + }
  120 + homeDashboardId = user.additionalInfo.homeDashboardId;
  121 + if (isDefinedAndNotNull(user.additionalInfo.homeDashboardHideToolbar)) {
  122 + homeDashboardHideToolbar = user.additionalInfo.homeDashboardHideToolbar;
  123 + }
  124 + }
  125 + if (!lang) {
112 lang = this.translate.currentLang; 126 lang = this.translate.currentLang;
113 } 127 }
114 this.profile.get('language').setValue(lang); 128 this.profile.get('language').setValue(lang);
  129 + this.profile.get('homeDashboardId').setValue(homeDashboardId);
  130 + this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar);
115 } 131 }
116 132
117 confirmForm(): FormGroup { 133 confirmForm(): FormGroup {
@@ -60,6 +60,21 @@ @@ -60,6 +60,21 @@
60 <mat-label translate>tenant.description</mat-label> 60 <mat-label translate>tenant.description</mat-label>
61 <textarea matInput formControlName="description" rows="2"></textarea> 61 <textarea matInput formControlName="description" rows="2"></textarea>
62 </mat-form-field> 62 </mat-form-field>
  63 + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
  64 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  65 + <tb-dashboard-autocomplete
  66 + fxFlex
  67 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  68 + formControlName="homeDashboardId"
  69 + [dashboardsScope]="'tenant'"
  70 + [tenantId]="entity?.id.id"
  71 + [selectFirstDashboard]="false"
  72 + ></tb-dashboard-autocomplete>
  73 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  74 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  75 + </mat-checkbox>
  76 + </section>
  77 + </section>
63 </div> 78 </div>
64 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> 79 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
65 </fieldset> 80 </fieldset>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + .tb-default-dashboard {
  20 + tb-dashboard-autocomplete {
  21 + @media #{$mat-gt-sm} {
  22 + padding-right: 12px;
  23 + }
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding-bottom: 12px;
  27 + }
  28 + }
  29 + mat-checkbox {
  30 + @media #{$mat-gt-sm} {
  31 + margin-top: 16px;
  32 + }
  33 + }
  34 + }
  35 +}
@@ -23,11 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti @@ -23,11 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
23 import { TranslateService } from '@ngx-translate/core'; 23 import { TranslateService } from '@ngx-translate/core';
24 import { ContactBasedComponent } from '../../components/entity/contact-based.component'; 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefinedAndNotNull } from '@core/utils';
26 27
27 @Component({ 28 @Component({
28 selector: 'tb-tenant', 29 selector: 'tb-tenant',
29 templateUrl: './tenant.component.html', 30 templateUrl: './tenant.component.html',
30 - styleUrls: [] 31 + styleUrls: ['./tenant.component.scss']
31 }) 32 })
32 export class TenantComponent extends ContactBasedComponent<TenantInfo> { 33 export class TenantComponent extends ContactBasedComponent<TenantInfo> {
33 34
@@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { @@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
54 tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]], 55 tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]],
55 additionalInfo: this.fb.group( 56 additionalInfo: this.fb.group(
56 { 57 {
57 - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] 58 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  59 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  60 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  61 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
58 } 62 }
59 ) 63 )
60 } 64 }
@@ -65,6 +69,11 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { @@ -65,6 +69,11 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
65 this.entityForm.patchValue({title: entity.title}); 69 this.entityForm.patchValue({title: entity.title});
66 this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId}); 70 this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId});
67 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); 71 this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  72 + this.entityForm.patchValue({additionalInfo:
  73 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  74 + this.entityForm.patchValue({additionalInfo:
  75 + {homeDashboardHideToolbar: entity.additionalInfo &&
  76 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
68 } 77 }
69 78
70 updateFormState() { 79 updateFormState() {
@@ -95,6 +95,20 @@ @@ -95,6 +95,20 @@
95 {{ 'user.always-fullscreen' | translate }} 95 {{ 'user.always-fullscreen' | translate }}
96 </mat-checkbox> 96 </mat-checkbox>
97 </section> 97 </section>
  98 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  99 + <tb-dashboard-autocomplete
  100 + fxFlex
  101 + placeholder="{{ 'dashboard.home-dashboard' | translate }}"
  102 + formControlName="homeDashboardId"
  103 + [dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
  104 + [tenantId]="entity?.tenantId?.id"
  105 + [customerId]="entity?.customerId?.id"
  106 + [selectFirstDashboard]="false"
  107 + ></tb-dashboard-autocomplete>
  108 + <mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
  109 + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
  110 + </mat-checkbox>
  111 + </section>
98 </section> 112 </section>
99 </div> 113 </div>
100 </fieldset> 114 </fieldset>
@@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model'; @@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model';
23 import { selectAuth } from '@core/auth/auth.selectors'; 23 import { selectAuth } from '@core/auth/auth.selectors';
24 import { map } from 'rxjs/operators'; 24 import { map } from 'rxjs/operators';
25 import { Authority } from '@shared/models/authority.enum'; 25 import { Authority } from '@shared/models/authority.enum';
26 -import { isUndefined } from '@core/utils'; 26 +import { isDefinedAndNotNull, isUndefined } from '@core/utils';
27 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; 27 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
28 28
29 @Component({ 29 @Component({
@@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent<User> { @@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent<User> {
74 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], 74 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
75 defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], 75 defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
76 defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], 76 defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
  77 + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
  78 + homeDashboardHideToolbar: [entity && entity.additionalInfo &&
  79 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
77 } 80 }
78 ) 81 )
79 } 82 }
@@ -89,6 +92,11 @@ export class UserComponent extends EntityComponent<User> { @@ -89,6 +92,11 @@ export class UserComponent extends EntityComponent<User> {
89 {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); 92 {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
90 this.entityForm.patchValue({additionalInfo: 93 this.entityForm.patchValue({additionalInfo:
91 {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}}); 94 {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}});
  95 + this.entityForm.patchValue({additionalInfo:
  96 + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
  97 + this.entityForm.patchValue({additionalInfo:
  98 + {homeDashboardHideToolbar: entity.additionalInfo &&
  99 + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
92 } 100 }
93 101
94 } 102 }
@@ -35,6 +35,7 @@ @@ -35,6 +35,7 @@
35 [isEditActionEnabled]="true" 35 [isEditActionEnabled]="true"
36 [isExportActionEnabled]="true" 36 [isExportActionEnabled]="true"
37 [isRemoveActionEnabled]="!isReadOnly" 37 [isRemoveActionEnabled]="!isReadOnly"
  38 + [disableWidgetInteraction]="true"
38 [callbacks]="dashboardCallbacks"></tb-dashboard> 39 [callbacks]="dashboardCallbacks"></tb-dashboard>
39 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons"> 40 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
40 </tb-footer-fab-buttons> 41 </tb-footer-fab-buttons>
@@ -28,12 +28,17 @@ class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFiel @@ -28,12 +28,17 @@ class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFiel
28 ); 28 );
29 }); 29 });
30 30
  31 + let row = false;
  32 + if (this.props.form.direction === 'row') {
  33 + row = true;
  34 + }
  35 +
31 return ( 36 return (
32 <FormControl component='fieldset' 37 <FormControl component='fieldset'
33 className={this.props.form.htmlClass} 38 className={this.props.form.htmlClass}
34 disabled={this.props.form.readonly}> 39 disabled={this.props.form.readonly}>
35 <FormLabel component='legend'>{this.props.form.title}</FormLabel> 40 <FormLabel component='legend'>{this.props.form.title}</FormLabel>
36 - <RadioGroup name={this.props.form.title} value={this.props.value} onChange={(e) => { 41 + <RadioGroup row={row} name={this.props.form.title} value={this.props.value} onChange={(e) => {
37 this.props.onChangeValidate(e); 42 this.props.onChangeValidate(e);
38 }}> 43 }}>
39 {items} 44 {items}
@@ -22,6 +22,7 @@ import { @@ -22,6 +22,7 @@ import {
22 KeyLabelItem 22 KeyLabelItem
23 } from '@shared/components/json-form/react/json-form.models'; 23 } from '@shared/components/json-form/react/json-form.models';
24 import { Mode } from 'rc-select/lib/interface'; 24 import { Mode } from 'rc-select/lib/interface';
  25 +import { deepClone } from '@core/utils';
25 26
26 interface ThingsboardRcSelectState extends JsonFormFieldState { 27 interface ThingsboardRcSelectState extends JsonFormFieldState {
27 currentValue: KeyLabelItem | KeyLabelItem[]; 28 currentValue: KeyLabelItem | KeyLabelItem[];
@@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar @@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
151 labelClass += ' tb-focused'; 152 labelClass += ' tb-focused';
152 } 153 }
153 let mode: Mode; 154 let mode: Mode;
154 - if (this.props.form.tags) {  
155 - mode = 'tags';  
156 - } else if (this.props.form.multiple) {  
157 - mode = 'multiple'; 155 + let value = this.state.currentValue;
  156 + if (this.props.form.tags || this.props.form.multiple) {
  157 + value = deepClone(value);
  158 + if (this.props.form.tags) {
  159 + mode = 'tags';
  160 + } else if (this.props.form.multiple) {
  161 + mode = 'multiple';
  162 + }
158 } 163 }
159 164
160 const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}}; 165 const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}};
@@ -176,12 +181,13 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar @@ -176,12 +181,13 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
176 maxTagTextLength={this.props.form.maxTagTextLength} 181 maxTagTextLength={this.props.form.maxTagTextLength}
177 disabled={this.props.form.readonly} 182 disabled={this.props.form.readonly}
178 optionLabelProp='children' 183 optionLabelProp='children'
179 - value={this.state.currentValue} 184 + value={value}
180 labelInValue={true} 185 labelInValue={true}
181 onSelect={this.onSelect} 186 onSelect={this.onSelect}
182 onDeselect={this.onDeselect} 187 onDeselect={this.onDeselect}
183 onFocus={this.onFocus} 188 onFocus={this.onFocus}
184 onBlur={this.onBlur} 189 onBlur={this.onBlur}
  190 + placeholder={this.props.form.placeholder}
185 style={this.props.form.style || {width: '100%'}}> 191 style={this.props.form.style || {width: '100%'}}>
186 {options} 192 {options}
187 </Select> 193 </Select>
@@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo { @@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo {
106 configuration?: DashboardConfiguration; 106 configuration?: DashboardConfiguration;
107 } 107 }
108 108
  109 +export interface HomeDashboard extends Dashboard {
  110 + hideDashboardToolbar: boolean;
  111 +}
  112 +
  113 +export interface HomeDashboardInfo {
  114 + dashboardId: DashboardId;
  115 + hideDashboardToolbar: boolean;
  116 +}
  117 +
109 export function isPublicDashboard(dashboard: DashboardInfo): boolean { 118 export function isPublicDashboard(dashboard: DashboardInfo): boolean {
110 if (dashboard && dashboard.assignedCustomers) { 119 if (dashboard && dashboard.assignedCustomers) {
111 return dashboard.assignedCustomers 120 return dashboard.assignedCustomers
@@ -74,6 +74,7 @@ @@ -74,6 +74,7 @@
74 "admin": { 74 "admin": {
75 "general": "General", 75 "general": "General",
76 "general-settings": "General Settings", 76 "general-settings": "General Settings",
  77 + "home-settings": "Home Settings",
77 "outgoing-mail": "Mail Server", 78 "outgoing-mail": "Mail Server",
78 "outgoing-mail-settings": "Outgoing Mail Server Settings", 79 "outgoing-mail-settings": "Outgoing Mail Server Settings",
79 "system-settings": "System Settings", 80 "system-settings": "System Settings",
@@ -764,7 +765,9 @@ @@ -764,7 +765,9 @@
764 "select-state": "Select target state", 765 "select-state": "Select target state",
765 "state-controller": "State controller", 766 "state-controller": "State controller",
766 "search": "Search dashboards", 767 "search": "Search dashboards",
767 - "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected" 768 + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected",
  769 + "home-dashboard": "Home dashboard",
  770 + "home-dashboard-hide-toolbar": "Hide home dashboard toolbar"
768 }, 771 },
769 "datakey": { 772 "datakey": {
770 "settings": "Settings", 773 "settings": "Settings",
@@ -1118,6 +1118,13 @@ @@ -1118,6 +1118,13 @@
1118 dependencies: 1118 dependencies:
1119 regenerator-runtime "^0.13.4" 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 "@babel/template@7.10.4", "@babel/template@^7.10.4": 1128 "@babel/template@7.10.4", "@babel/template@^7.10.4":
1122 version "7.10.4" 1129 version "7.10.4"
1123 resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" 1130 resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
@@ -7941,7 +7948,7 @@ rc-util@^4.15.3: @@ -7941,7 +7948,7 @@ rc-util@^4.15.3:
7941 react-lifecycles-compat "^3.0.4" 7948 react-lifecycles-compat "^3.0.4"
7942 shallowequal "^1.1.0" 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 version "5.4.0" 7952 version "5.4.0"
7946 resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004" 7953 resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004"
7947 integrity sha512-kXDn1JyLJTAWLBFt+fjkTcUtXhxKkipQCobQmxIEVrX62iXgo24z8YKoWehWfMxPZFPE+RXqrmEu9j5kHz/Lrg== 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,6 +7956,15 @@ rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.3.0:
7949 react-is "^16.12.0" 7956 react-is "^16.12.0"
7950 shallowequal "^1.1.0" 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 rc-virtual-list@^1.1.2: 7968 rc-virtual-list@^1.1.2:
7953 version "1.1.6" 7969 version "1.1.6"
7954 resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.6.tgz#b255baf9aacde149a8893324e6307214094f4c0a" 7970 resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.6.tgz#b255baf9aacde149a8893324e6307214094f4c0a"