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