diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 4930219741..1cf107b801 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -3,7 +3,9 @@ "alias": "cards", "title": "Cards", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAT5UlEQVR42u3d95MUVffHcf86/UEtqwxVapmzaH0VE2YR82PAgBkTKCYUAyrGR0mSxERwBXRV0EdFRcAc5vvaPnptZ5bdZWbDwJ5TW1s93T23b5/7vufe7t3PuXv8+OOP36YNZrzUqCzdNUR37fHdd9810gaz4qV01xDdlWAlWAlWgpVgJVgJVnoqwUqwEqwEK8FKsNISrAQrwdqlwfrtt9/+V7Off/55gIJ++OGH8m46bOvWrb/88sv4Aeubb76pu8vb5z/++MPvEaqPwocL8RUrVqhwfc8rr7zSYdsNBFZvb+/hhx/+n79t/fr1ree8+eabn376qY03KrPx2muvff311zaeffbZd955Z/yANWvWLF76v8psPPLII19++eVdd901QvX57LPP7rvvvqGfv3nz5ldffbXfQzfffPPy5cvre9zC999/P4JgnXfeefU9n3/+uX6pEkHMxo0bL7vsMh5UCU786quvfGXSpElz584VwByNkvUtX3nrrbdsRDmrVq1auHDhtm3bdr+h8LHHHpszZ05s//TTT9EbN2zYwG+LFy/mH+OACMGBf/75Z5zW09Pj0Pbt2wsxuuimTZvi46+//qr3vvvuu3H+hx9++MUXX/g6D3/88cdxzkcffbRo0aItW7bER+FHCa5VamW0ef75588555z4iootW7Zs5cqV0SLA0hyuUmJHAUutHOo3pnQElqpsq8xt2DNjxowzzjjjySefPP/8819++eUPPvjAxh133AGpJ5544plnnnHDp512mtNUa/r06QsWLOCOKVOmPPTQQ7rXDTfcoJCHH374+uuvd/KECROaRs/dDCwOvPjii21Mnjz58ssvF8JPPvnkK664wsYll1zCAw5xpvD21FNPcazRR5fj8xdeeIEbuff333/nYWXedttt/On8s88++8ILL5w3b56jV155ZQxbPKxMNJh+QEdRgVGJQ1pw5syZTnj77beRfdZZZ6nk7bffrtgAK/b4CowKWO5UUUoWPlxlOME69NBDL67MtQOsl156yYYa33rrrTbUbMmSJTYCLBvuVhe0EWDpYVwT0w5u1VduvPFG96AP6cfjByxBJToVkmysXr0aTwLGUUcdJbRzztSpU5cuXWq0uvba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", - "description": "Tables and cards to display latest and historical values for multiple entities simultaneously." + "description": "Tables and cards to display latest and historical values for multiple entities simultaneously.", + "externalId": null, + "name": "Cards" }, "widgetTypes": [ { @@ -100,7 +102,9 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-simple-card-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-simple-card-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\"}" } }, { diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index de32604dbb..7ccce1d8c6 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -3,7 +3,9 @@ "alias": "charts", "title": "Charts", "image": "data:image/png;base64,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", - "description": "Display timeseries data using customizable line and bar charts. Use various pie charts to display latest values." + "description": "Display timeseries data using customizable line and bar charts. Use various pie charts to display latest values.", + "externalId": null, + "name": "Charts" }, "widgetTypes": [ { @@ -151,15 +153,15 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "", + "templateHtml": "\n", "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", - "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.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-line-widget-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "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}}" + "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\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"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}}" } }, { @@ -172,16 +174,16 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "", + "templateHtml": "\n", "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", - "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.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-line-widget-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "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 Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" + "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\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" } }, { @@ -194,15 +196,15 @@ "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "", + "templateHtml": "\n", "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", - "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.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-bar-widget-settings", "dataKeySettingsDirective": "tb-flot-bar-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", - "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 Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" + "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\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" } } ] diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index af0e64ddb6..be08d50d69 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -27,7 +27,13 @@ import { AlarmFilter, AlarmFilterConfig, createDefaultEntityDataPageLink, - Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, + Filter, + FilterInfo, + filterInfoToKeyFilters, + Filters, + KeyFilter, + singleEntityDataPageLink, + singleEntityFilterFromDeviceId, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; import { TranslateService } from '@ngx-translate/core'; @@ -246,6 +252,9 @@ export class AliasController implements IAliasController { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { const newDatasource = deepClone(datasource); + if (newDatasource.type === DatasourceType.device) { + newDatasource.type = DatasourceType.entity; + } if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount || newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.filterId) { @@ -254,7 +263,22 @@ export class AliasController implements IAliasController { if (newDatasource.type === DatasourceType.alarmCount) { newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); } - if (newDatasource.entityAliasId) { + if (newDatasource.deviceId) { + newDatasource.entityFilter = singleEntityFilterFromDeviceId(newDatasource.deviceId); + if (forceFilter) { + return this.entityService.findSingleEntityInfoByEntityFilter(newDatasource.entityFilter, + {ignoreLoading: true, ignoreErrors: true}).pipe( + map((entity) => { + if (entity) { + updateDatasourceFromEntityInfo(newDatasource, entity, true); + } + return newDatasource; + }) + ); + } else { + return of(newDatasource); + } + } else if (newDatasource.entityAliasId) { return this.getAliasInfo(newDatasource.entityAliasId).pipe( mergeMap((aliasInfo) => { newDatasource.aliasName = aliasInfo.alias; diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 5a112f8336..243c5e369b 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -29,20 +29,25 @@ import { GridSettings, WidgetLayout } from '@shared/models/dashboard.models'; -import { isDefined, isString, isUndefined } from '@core/utils'; +import { deepClone, isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils'; import { + DataKey, Datasource, datasourcesHasOnlyComparisonAggregation, DatasourceType, + defaultLegendConfig, Widget, WidgetConfig, - widgetType + WidgetConfigMode, + widgetType, + WidgetTypeDescriptor } from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { EntityId } from '@app/shared/models/id/entity-id'; import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @Injectable({ providedIn: 'root' @@ -211,9 +216,26 @@ export class DashboardUtilsService { public validateAndUpdateWidget(widget: Widget): Widget { widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); // Temp workaround - if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { + if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { widget.typeAlias = 'basic_timeseries'; } + if (widget.isSystemType && widget.bundleAlias === 'charts' && + ['state_chart', 'basic_timeseries', 'timeseries_bars_flot'].includes(widget.typeAlias)) { + const widgetConfig = widget.config; + const widgetSettings = widget.config.settings; + if (isDefinedAndNotNull(widgetConfig.showLegend)) { + widgetSettings.showLegend = widgetConfig.showLegend; + delete widgetConfig.showLegend; + } else if (isUndefined(widgetSettings.showLegend)) { + widgetSettings.showLegend = true; + } + if (isDefinedAndNotNull(widgetConfig.legendConfig)) { + widgetSettings.legendConfig = widgetConfig.legendConfig; + delete widgetConfig.legendConfig; + } else if (isUndefined(widgetSettings.legendConfig)) { + widgetSettings.legendConfig = defaultLegendConfig(widget.type); + } + } return widget; } @@ -225,10 +247,8 @@ export class DashboardUtilsService { widgetConfig.datasources = []; } widgetConfig.datasources.forEach((datasource) => { - if (datasource.type === 'device') { - datasource.type = DatasourceType.entity; - } if (datasource.deviceAliasId) { + datasource.type = DatasourceType.entity; datasource.entityAliasId = datasource.deviceAliasId; delete datasource.deviceAliasId; } @@ -316,6 +336,55 @@ export class DashboardUtilsService { }; } + public widgetConfigFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor): WidgetConfig { + const config: WidgetConfig = JSON.parse(widgetTypeDescriptor.defaultConfig); + config.datasources = this.convertDatasourcesFromWidgetType(widgetTypeDescriptor, config, config.datasources); + if (isDefinedAndNotNull(config.alarmSource)) { + config.alarmSource = this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, config.alarmSource); + } + return config; + } + + private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, + config: WidgetConfig, datasources?: Datasource[]): Datasource[] { + const newDatasources: Datasource[] = []; + if (datasources) { + datasources.forEach(datasource => { + newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasource)); + }); + } + return newDatasources; + } + + private convertDatasourceFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, + datasource: Datasource): Datasource { + const newDatasource = deepClone(datasource); + if (newDatasource.type === DatasourceType.function) { + newDatasource.type = DatasourceType.entity; + if (widgetTypeDescriptor.hasBasicMode && config.configMode === WidgetConfigMode.basic) { + newDatasource.type = DatasourceType.device; + } + const dataKeys = newDatasource.dataKeys; + newDatasource.dataKeys = []; + dataKeys.forEach(dataKey => { + newDatasource.dataKeys.push(this.convertDataKeyFromWidgetType(widgetTypeDescriptor, config, dataKey)); + }); + } + return newDatasource; + } + + private convertDataKeyFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, dataKey: DataKey): DataKey { + const newDataKey = deepClone(dataKey); + newDataKey.funcBody = null; + if (widgetTypeDescriptor.type === widgetType.alarm) { + newDataKey.type = DataKeyType.alarm; + } else { + newDataKey.type = DataKeyType.timeseries; + newDataKey.name = newDataKey.label; + } + return newDataKey; + } + private validateAndUpdateState(state: DashboardState) { if (!state.layouts) { state.layouts = this.createDefaultLayouts(); @@ -429,7 +498,7 @@ export class DashboardUtilsService { targetLayout: DashboardLayoutId, widget: Widget, originalColumns?: number, - originalSize?: {sizeX: number, sizeY: number}, + originalSize?: {sizeX: number; sizeY: number}, row?: number, column?: number): void { const dashboardConfiguration = dashboard.configuration; @@ -502,7 +571,7 @@ export class DashboardUtilsService { this.removeUnusedWidgets(dashboard); } - public isSingleLayoutDashboard(dashboard: Dashboard): {state: string, layout: DashboardLayoutId} { + public isSingleLayoutDashboard(dashboard: Dashboard): {state: string; layout: DashboardLayoutId} { const dashboardConfiguration = dashboard.configuration; const states = dashboardConfiguration.states; const stateKeys = Object.keys(states); diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 40172e2667..b3a14b0f61 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -196,7 +196,7 @@ import * as AddAttributeDialogComponent from '@home/components/attribute/add-att import * as EditAttributeValuePanelComponent from '@home/components/attribute/edit-attribute-value-panel.component'; import * as DashboardComponent from '@home/components/dashboard/dashboard.component'; import * as WidgetComponent from '@home/components/widget/widget.component'; -import * as LegendComponent from '@home/components/widget/legend.component'; +import * as LegendComponent from '@home/components/widget/lib/legend.component'; import * as AliasesEntitySelectPanelComponent from '@home/components/alias/aliases-entity-select-panel.component'; import * as AliasesEntitySelectComponent from '@home/components/alias/aliases-entity-select.component'; import * as WidgetConfigComponent from '@home/components/widget/widget-config.component'; @@ -206,10 +206,10 @@ import * as EntityAliasDialogComponent from '@home/components/alias/entity-alias import * as EntityFilterComponent from '@home/components/entity/entity-filter.component'; import * as RelationFiltersComponent from '@home/components/relation/relation-filters.component'; import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias-select.component'; -import * as DataKeysComponent from '@home/components/widget/data-keys.component'; -import * as DataKeyConfigDialogComponent from '@home/components/widget/data-key-config-dialog.component'; -import * as DataKeyConfigComponent from '@home/components/widget/data-key-config.component'; -import * as LegendConfigComponent from '@home/components/widget/legend-config.component'; +import * as DataKeysComponent from '@home/components/widget/config/data-keys.component'; +import * as DataKeyConfigDialogComponent from '@home/components/widget/config/data-key-config-dialog.component'; +import * as DataKeyConfigComponent from '@home/components/widget/config/data-key-config.component'; +import * as LegendConfigComponent from '@home/components/widget/lib/settings/common/legend-config.component'; import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component'; import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component'; import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; @@ -491,7 +491,7 @@ class ModulesMap implements IModulesMap { '@home/components/attribute/edit-attribute-value-panel.component': EditAttributeValuePanelComponent, '@home/components/dashboard/dashboard.component': DashboardComponent, '@home/components/widget/widget.component': WidgetComponent, - '@home/components/widget/legend.component': LegendComponent, + '@home/components/widget/lib/legend.component': LegendComponent, '@home/components/alias/aliases-entity-select-panel.component': AliasesEntitySelectPanelComponent, '@home/components/alias/aliases-entity-select.component': AliasesEntitySelectComponent, '@home/components/widget/widget-config.component': WidgetConfigComponent, @@ -501,10 +501,10 @@ class ModulesMap implements IModulesMap { '@home/components/entity/entity-filter.component': EntityFilterComponent, '@home/components/relation/relation-filters.component': RelationFiltersComponent, '@home/components/alias/entity-alias-select.component': EntityAliasSelectComponent, - '@home/components/widget/data-keys.component': DataKeysComponent, - '@home/components/widget/data-key-config-dialog.component': DataKeyConfigDialogComponent, - '@home/components/widget/data-key-config.component': DataKeyConfigComponent, - '@home/components/widget/legend-config.component': LegendConfigComponent, + '@home/components/widget/config/data-keys.component': DataKeysComponent, + '@home/components/widget/config/data-key-config-dialog.component': DataKeyConfigDialogComponent, + '@home/components/widget/config/data-key-config.component': DataKeyConfigComponent, + '@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent, '@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent, '@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent, '@home/components/widget/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent, diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html index db13e8dcee..574209c6f8 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html @@ -31,6 +31,13 @@ (click)="clear()"> close + diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts index e0246cbda3..be8b8d66cb 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts @@ -48,11 +48,11 @@ import { ErrorStateMatcher } from '@angular/material/core'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EntityAliasSelectComponent), multi: true - }, + }/*, { provide: ErrorStateMatcher, useExisting: EntityAliasSelectComponent - }] + }*/] }) export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { @@ -237,16 +237,19 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, } } - createEntityAlias($event: Event, alias: string) { + createEntityAlias($event: Event, alias: string, focusOnCancel = true) { $event.preventDefault(); + $event.stopPropagation(); this.creatingEntityAlias = true; if (this.callbacks && this.callbacks.createEntityAlias) { this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => { if (!newAlias) { - setTimeout(() => { - this.entityAliasInput.nativeElement.blur(); - this.entityAliasInput.nativeElement.focus(); - }, 0); + if (focusOnCancel) { + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } } else { this.entityAliasList.push(newAlias); this.modelValue = newAlias.id; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index 22bb497c12..fb5336de66 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -15,11 +15,25 @@ limitations under the License. --> -
+

widget.add

: {{data.widgetInfo.widgetName}} -
+
+ + +
+
+ + -
+
- +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 27291b49b9..ada22bba94 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -22,15 +22,17 @@ import { AppState } from '@core/core.state'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; -import { Widget, widgetTypesData } from '@shared/models/widget.models'; +import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models'; import { Dashboard } from '@app/shared/models/dashboard.models'; -import { IAliasController } from '@core/api/widget-api.models'; +import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; -import { isDefined, isString } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; export interface AddWidgetDialogData { dashboard: Dashboard; aliasController: IAliasController; + stateController: IStateController; widget: Widget; widgetInfo: WidgetInfo; } @@ -38,22 +40,43 @@ export interface AddWidgetDialogData { @Component({ selector: 'tb-add-widget-dialog', templateUrl: './add-widget-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}], + providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/], styleUrls: [] }) export class AddWidgetDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { + widgetConfigModes = WidgetConfigMode; + widgetFormGroup: UntypedFormGroup; dashboard: Dashboard; aliasController: IAliasController; + stateController: IStateController; widget: Widget; + widgetConfig: WidgetConfigComponentData; + + previewMode = false; + + hasBasicMode = false; + + get widgetConfigMode(): WidgetConfigMode { + return this.hasBasicMode ? (this.widgetConfig?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced; + } + + set widgetConfigMode(widgetConfigMode: WidgetConfigMode) { + if (this.hasBasicMode) { + this.widgetConfig.config.configMode = widgetConfigMode; + this.widgetFormGroup.markAsDirty(); + } + } + submitted = false; constructor(protected store: Store, protected router: Router, + public translate: TranslateService, @Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, @@ -62,6 +85,7 @@ export class AddWidgetDialogComponent extends DialogComponent -
+ [isReadOnly]="true" + (closeDetails)="onEditWidgetClosed()"> +
+ +
+ [widgetLayout]="editingWidgetLayout" + (revertWidgetConfig)="onRevertWidgetEdit()" + (applyWidgetConfig)="saveWidget()"> { - const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig); + const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo); config.title = 'New ' + widgetTypeInfo.widgetName; - config.datasources = []; - if (isDefinedAndNotNull(config.alarmSource)) { - config.alarmSource = { - type: DatasourceType.entity, - dataKeys: config.alarmSource.dataKeys || [] - }; - } let newWidget: Widget = { isSystemType: widget.isSystemType, bundleAlias: widget.bundleAlias, @@ -1176,6 +1169,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC data: { dashboard: this.dashboard, aliasController: this.dashboardCtx.aliasController, + stateController: this.dashboardCtx.stateController, widget: newWidget, widgetInfo: widgetTypeInfo } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html index c7c2465230..837c0abcad 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html @@ -22,7 +22,43 @@ [functionsOnly]="widgetEditMode" [dashboard]="dashboard" [widget]="widget" + [hideToggleHeader]="previewMode" + [widgetConfigMode]="widgetConfigMode" formControlName="widgetConfig"> +
+ +
+
+ + + +
+ + diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss new file mode 100644 index 0000000000..9777decdb0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .widget-preview-section { + position: absolute; + top: 72px; + left: 16px; + right: 16px; + bottom: 16px; + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: 4px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts index a9f6805a98..2528ada4ae 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts @@ -14,23 +14,23 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { MatDialog } from '@angular/material/dialog'; import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models'; -import { IAliasController } from '@core/api/widget-api.models'; -import { Widget } from '@shared/models/widget.models'; +import { IAliasController, IStateController } from '@core/api/widget-api.models'; +import { Widget, WidgetConfigMode } from '@shared/models/widget.models'; import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetConfigComponentData } from '../../models/widget-component.models'; -import { isDefined, isString } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; @Component({ selector: 'tb-edit-widget', templateUrl: './edit-widget.component.html', - styleUrls: [] + styleUrls: ['./edit-widget.component.scss'] }) export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges { @@ -40,6 +40,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan @Input() aliasController: IAliasController; + @Input() + stateController: IStateController; + @Input() widgetEditMode: boolean; @@ -49,10 +52,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan @Input() widgetLayout: WidgetLayout; + @Output() + applyWidgetConfig = new EventEmitter(); + + @Output() + revertWidgetConfig = new EventEmitter(); + widgetFormGroup: UntypedFormGroup; widgetConfig: WidgetConfigComponentData; + previewMode = false; + + hasBasicMode = false; + + get widgetConfigMode(): WidgetConfigMode { + return this.hasBasicMode ? (this.widgetConfig?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced; + } + + set widgetConfigMode(widgetConfigMode: WidgetConfigMode) { + if (this.hasBasicMode) { + this.widgetConfig.config.configMode = widgetConfigMode; + this.widgetFormGroup.markAsDirty(); + } + } + + private currentWidgetConfigChanged = false; + constructor(protected store: Store, private dialog: MatDialog, private fb: UntypedFormBuilder, @@ -78,10 +104,27 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan } } if (reloadConfig) { + if (this.currentWidgetConfigChanged) { + this.currentWidgetConfigChanged = false; + } else { + this.previewMode = false; + } this.loadWidgetConfig(); } } + onApplyWidgetConfig() { + if (this.widgetFormGroup.valid) { + this.currentWidgetConfigChanged = true; + this.applyWidgetConfig.emit(); + } + } + + onRevertWidgetConfig() { + this.currentWidgetConfigChanged = true; + this.revertWidgetConfig.emit(); + } + private loadWidgetConfig() { if (!this.widget) { return; @@ -113,6 +156,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema; } this.widgetConfig = { + widgetName: widgetInfo.widgetName, config: this.widget.config, layout: this.widgetLayout, widgetType: this.widget.type, @@ -124,8 +168,10 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan latestDataKeySettingsSchema, settingsDirective: widgetInfo.settingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, - latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective + latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, + basicModeDirective: widgetInfo.basicModeDirective }; + this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false; this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.html index 9569eb6c9b..016c87f40c 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.html @@ -31,6 +31,13 @@ (click)="clear()"> close + diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts index 8867f554f7..6c844e4a9b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts @@ -226,16 +226,19 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte } } - createFilter($event: Event, filter: string) { + createFilter($event: Event, filter: string, focusOnCancel = true) { $event.preventDefault(); + $event.stopPropagation(); this.creatingFilter = true; if (this.callbacks && this.callbacks.createFilter) { this.callbacks.createFilter(filter).subscribe((newFilter) => { if (!newFilter) { - setTimeout(() => { - this.filterInput.nativeElement.blur(); - this.filterInput.nativeElement.focus(); - }, 0); + if (focusOnCancel) { + setTimeout(() => { + this.filterInput.nativeElement.blur(); + this.filterInput.nativeElement.focus(); + }, 0); + } } else { this.filterList.push(newFilter); this.modelValue = newFilter.id; diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index ffa2d11b99..a6e2cc03dc 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -36,7 +36,6 @@ import { EditAttributeValuePanelComponent } from '@home/components/attribute/edi import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; import { WidgetComponent } from '@home/components/widget/widget.component'; import { WidgetComponentService } from '@home/components/widget/widget-component.service'; -import { LegendComponent } from '@home/components/widget/legend.component'; import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; @@ -45,11 +44,6 @@ import { EntityFilterViewComponent } from '@home/components/entity/entity-filter import { EntityAliasDialogComponent } from '@home/components/alias/entity-alias-dialog.component'; import { EntityFilterComponent } from '@home/components/entity/entity-filter.component'; import { RelationFiltersComponent } from '@home/components/relation/relation-filters.component'; -import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component'; -import { DataKeysComponent } from '@home/components/widget/data-keys.component'; -import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component'; -import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; -import { LegendConfigComponent } from '@home/components/widget/legend-config.component'; import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component'; import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; @@ -78,7 +72,6 @@ import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/c import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component'; import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component'; import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component'; -import { FilterSelectComponent } from '@home/components/filter/filter-select.component'; import { FiltersEditComponent } from '@home/components/filter/filters-edit.component'; import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component'; import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component'; @@ -152,8 +145,6 @@ import { AlarmDynamicValue } from '@home/components/profile/alarm/alarm-dynamic- import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component'; import { QueueFormComponent } from '@home/components/queue/queue-form.component'; -import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; -import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component'; @@ -178,10 +169,14 @@ import { modulesMap } from '@modules/common/modules-map'; import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; -import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component'; import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component'; -import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component'; +import { WidgetPreviewComponent } from '@home/components/widget/widget-preview.component'; +import { + ManageWidgetActionsDialogComponent +} from '@home/components/widget/action/manage-widget-actions-dialog.component'; +import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; +import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; @NgModule({ declarations: @@ -206,9 +201,7 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f AlarmTableHeaderComponent, AlarmTableComponent, AlarmAssigneePanelComponent, - AlarmAssigneeSelectComponent, AlarmAssigneeSelectPanelComponent, - AlarmFilterConfigComponent, AttributeTableComponent, AddAttributeDialogComponent, EditAttributeValuePanelComponent, @@ -220,18 +213,13 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f DashboardComponent, WidgetContainerComponent, WidgetComponent, - LegendComponent, - WidgetSettingsComponent, WidgetConfigComponent, + WidgetPreviewComponent, EntityFilterViewComponent, EntityFilterComponent, - EntityAliasSelectComponent, - DataKeysComponent, - DataKeyConfigComponent, - DataKeyConfigDialogComponent, - LegendConfigComponent, ManageWidgetActionsComponent, WidgetActionDialogComponent, + ManageWidgetActionsDialogComponent, CustomActionPrettyResourcesTabsComponent, CustomActionPrettyEditorComponent, MobileActionEditorComponent, @@ -253,7 +241,6 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f KeyFilterDialogComponent, FilterDialogComponent, FiltersDialogComponent, - FilterSelectComponent, FilterTextComponent, FiltersEditComponent, FiltersEditPanelComponent, @@ -338,7 +325,8 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f CommonModule, SharedModule, SharedHomeComponentsModule, - WidgetSettingsModule, + WidgetConfigComponentsModule, + BasicWidgetConfigModule, Lwm2mProfileComponentsModule, SnmpDeviceProfileTransportModule, StatesControllerModule, @@ -360,9 +348,7 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f RelationFiltersComponent, AlarmTableComponent, AlarmAssigneePanelComponent, - AlarmAssigneeSelectComponent, AlarmAssigneeSelectPanelComponent, - AlarmFilterConfigComponent, AttributeTableComponent, AliasesEntitySelectComponent, AliasesEntityAutocompleteComponent, @@ -371,18 +357,13 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f DashboardComponent, WidgetContainerComponent, WidgetComponent, - LegendComponent, - WidgetSettingsComponent, WidgetConfigComponent, + WidgetPreviewComponent, EntityFilterViewComponent, EntityFilterComponent, - EntityAliasSelectComponent, - DataKeysComponent, - DataKeyConfigComponent, - DataKeyConfigDialogComponent, - LegendConfigComponent, ManageWidgetActionsComponent, WidgetActionDialogComponent, + ManageWidgetActionsDialogComponent, CustomActionPrettyResourcesTabsComponent, CustomActionPrettyEditorComponent, MobileActionEditorComponent, @@ -403,7 +384,6 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f KeyFilterDialogComponent, FilterDialogComponent, FiltersDialogComponent, - FilterSelectComponent, FilterTextComponent, FiltersEditComponent, UserFilterDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html new file mode 100644 index 0000000000..36f58f27c6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -0,0 +1,52 @@ + +
+ +

{{ data.widgetTitle }}: {{ 'widget-config.actions' | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts new file mode 100644 index 0000000000..0133c375f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { widgetType } from '@shared/models/widget.models'; +import { + WidgetActionCallbacks, + WidgetActionsData +} from '@home/components/widget/action/manage-widget-actions.component.models'; +import { Component, Inject, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; + +export interface ManageWidgetActionsDialogData { + widgetTitle: string; + actionsData: WidgetActionsData; + callbacks: WidgetActionCallbacks; + widgetType: widgetType; +} + +@Component({ + selector: 'tb-manage-widget-actions-dialog', + templateUrl: './manage-widget-actions-dialog.component.html', + providers: [], + styleUrls: [] +}) +export class ManageWidgetActionsDialogComponent extends DialogComponent implements OnInit { + + actionSources = this.data.actionsData.actionSources; + actionsSettings: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ManageWidgetActionsDialogData, + private fb: UntypedFormBuilder, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + ngOnInit() { + this.actionsSettings = this.fb.group({ + actions: [this.data.actionsData.actionsMap, []] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.actionsSettings.get('actions').value); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html index 7a85c37973..1c29a715b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -15,8 +15,8 @@ limitations under the License. --> -
-
+
+
widget-config.actions @@ -69,7 +69,7 @@ class="tb-drop-list"> - +
{{ 'widget-config.action-icon' | translate }} - + {{ action.icon }} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss index a42feda512..108e27ab13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss @@ -24,12 +24,6 @@ background: #fff; overflow: hidden; - &.tb-outlined-border { - box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); - border: solid 1px #e0e0e0; - border-radius: 4px; - } - .tb-entity-table-title { padding-right: 20px; white-space: nowrap; @@ -49,6 +43,12 @@ opacity: 1 !important; } + .mat-mdc-cell.tb-icon-cell { + .mat-icon { + vertical-align: middle; + } + } + .tb-draggable { &.cdk-drag-placeholder { opacity: 0; diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index d18c1319f4..0db42badf2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -76,6 +76,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni @Input() callbacks: WidgetActionCallbacks; + @Input() actionSources: {[actionSourceId: string]: WidgetActionSource}; + innerValue: WidgetActionsData; displayedColumns: string[]; @@ -89,6 +91,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni dragDisabled = true; private widgetResize$: ResizeObserver; + private destroyed = false; @ViewChild('searchInput') searchInputField: ElementRef; @@ -123,6 +126,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } ngOnDestroy(): void { + this.destroyed = true; if (this.widgetResize$) { this.widgetResize$.disconnect(); } @@ -336,22 +340,25 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni this.disabled = isDisabled; } - writeValue(obj: WidgetActionsData): void { - this.innerValue = obj; - if (this.innerValue) { - setTimeout(() => { + writeValue(actions?: {[actionSourceId: string]: Array}): void { + this.innerValue = { + actionsMap: actions || {}, + actionSources: this.actionSources || {} + }; + setTimeout(() => { + if (!this.destroyed) { this.dataSource.setActions(this.innerValue); if (this.viewsInited) { this.resetSortAndFilter(true); } else { this.dirtyValue = true; } - }, 0); - } + } + }, 0); } private onActionsUpdated() { this.updateData(true); - this.propagateChange(this.innerValue); + this.propagateChange(this.innerValue.actionsMap); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss new file mode 100644 index 0000000000..d29594fce3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + flex-direction: column; + gap: 16px; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts new file mode 100644 index 0000000000..e7d0d58b13 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule, Type } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { IBasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; +import { + SimpleCardBasicConfigComponent +} from '@home/components/widget/config/basic/cards/simple-card-basic-config.component'; +import { + WidgetActionsPanelComponent +} from '@home/components/widget/config/basic/common/widget-actions-panel.component'; + +@NgModule({ + declarations: [ + WidgetActionsPanelComponent, + SimpleCardBasicConfigComponent + ], + imports: [ + CommonModule, + SharedModule, + WidgetConfigComponentsModule + ], + exports: [ + WidgetActionsPanelComponent, + SimpleCardBasicConfigComponent + ] +}) +export class BasicWidgetConfigModule { +} + +export const basicWidgetConfigComponentsMap: {[key: string]: Type} = { + 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html new file mode 100644 index 0000000000..28cd0aaa6e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html @@ -0,0 +1,86 @@ + + + + + + +
+
widget-config.appearance
+
+
widgets.simple-card.label
+ + + +
+
+
widgets.simple-card.label-position
+ + + + {{ 'widgets.simple-card.label-position-left' | translate }} + + + {{ 'widgets.simple-card.label-position-top' | translate }} + + + +
+
+
widget-config.units-short
+ + +
+
+
widget-config.decimals-short
+ + + +
+
+
{{ 'widget-config.text-color' | translate }}
+
+ + + +
+
+
+
{{ 'widget-config.background' | translate }}
+
+ + + +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts new file mode 100644 index 0000000000..62d25249e9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts @@ -0,0 +1,110 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + Datasource, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation +} from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-simple-card-basic-config', + templateUrl: './simple-card-basic-config.component.html', + styleUrls: ['../basic-config.scss', '../../widget-config.scss'] +}) +export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { + + public get displayTimewindowConfig(): boolean { + const datasources = this.simpleCardWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.simpleCardWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + simpleCardWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected configForm(): UntypedFormGroup { + return this.simpleCardWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + this.simpleCardWidgetConfigForm = this.fb.group({ + timewindowConfig: [{ + useDashboardTimewindow: configData.config.useDashboardTimewindow, + displayTimewindow: configData.config.useDashboardTimewindow, + timewindow: configData.config.timewindow + }, []], + datasources: [configData.config.datasources, []], + label: [this.getDataKeyLabel(configData.config.datasources), []], + labelPosition: [configData.config.settings?.labelPosition, []], + units: [configData.config.units, []], + decimals: [configData.config.decimals, []], + color: [configData.config.color, []], + backgroundColor: [configData.config.backgroundColor, []], + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; + this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; + this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + this.widgetConfig.config.datasources = config.datasources; + this.setDataKeyLabel(config.label, this.widgetConfig.config.datasources); + this.widgetConfig.config.actions = config.actions; + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.color = config.color; + this.widgetConfig.config.backgroundColor = config.backgroundColor; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.labelPosition = config.labelPosition; + return this.widgetConfig; + } + + private getDataKeyLabel(datasources?: Datasource[]): string { + if (datasources && datasources.length) { + const dataKeys = datasources[0].dataKeys; + if (dataKeys && dataKeys.length) { + return dataKeys[0].label; + } + } + return ''; + } + + private setDataKeyLabel(label: string, datasources?: Datasource[]) { + if (datasources && datasources.length) { + const dataKeys = datasources[0].dataKeys; + if (dataKeys && dataKeys.length) { + dataKeys[0].label = label; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html new file mode 100644 index 0000000000..95e71efd5e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html @@ -0,0 +1,36 @@ + +
+
+
widget-config.actions
+ + + + {{ widgetAction.icon }} + {{ widgetAction.name }} + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts new file mode 100644 index 0000000000..68dfca5858 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { WidgetActionsData } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { WidgetActionDescriptor } from '@shared/models/widget.models'; +import { + ManageWidgetActionsDialogComponent, + ManageWidgetActionsDialogData +} from '@home/components/widget/action/manage-widget-actions-dialog.component'; +import { deepClone } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-widget-actions-panel', + templateUrl: './widget-actions-panel.component.html', + styleUrls: ['../../widget-config.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetActionsPanelComponent), + multi: true + } + ] +}) +export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + actionsFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.actionsFormGroup = this.fb.group({ + actions: [null, []] + }); + this.actionsFormGroup.get('actions').valueChanges.subscribe( + (val) => this.propagateChange(val) + ); + } + + writeValue(actions?: {[actionSourceId: string]: Array}): void { + this.actionsFormGroup.get('actions').patchValue(actions || {}, {emitEvent: false}); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.actionsFormGroup.disable({emitEvent: false}); + } else { + this.actionsFormGroup.enable({emitEvent: false}); + } + } + + public get widgetActionSourceIds(): Array { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + return actions ? Object.keys(actions) : []; + } + + public widgetActionsByActionSourceId(actionSourceId: string): Array { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + return actions[actionSourceId] || []; + } + + public get hasWidgetActions(): boolean { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + if (actions) { + for (const actionSourceId of Object.keys(actions)) { + if (actions[actionSourceId] && actions[actionSourceId].length) { + return true; + } + } + } + return false; + } + + public manageWidgetActions() { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + const actionsData: WidgetActionsData = { + actionsMap: deepClone(actions), + actionSources: this.widgetConfigComponent.modelValue.actionSources || {} + }; + this.dialog.open}>(ManageWidgetActionsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + widgetTitle: this.widgetConfigComponent.modelValue.widgetName, + callbacks: this.widgetConfigComponent.widgetConfigCallbacks, + actionsData, + widgetType: this.widgetConfigComponent.widgetType + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.actionsFormGroup.get('actions').patchValue(res); + this.cd.markForCheck(); + } + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html similarity index 90% rename from ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html rename to ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html index 01b928e354..354008393e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html @@ -32,6 +32,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts similarity index 95% rename from ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts index 019fc04aca..d2bdd2f8a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts @@ -24,7 +24,7 @@ import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; import { DataKeysCallbacks } from './data-keys.component.models'; -import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; +import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; @@ -36,9 +36,14 @@ export interface DataKeyConfigDialogData { aliasController: IAliasController; widget: Widget; widgetType: widgetType; + deviceId?: string; entityAliasId?: string; showPostProcessing?: boolean; callbacks?: DataKeysCallbacks; + hideDataKeyLabel: boolean; + hideDataKeyColor: boolean; + hideDataKeyUnits: boolean; + hideDataKeyDecimals: boolean; } @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html similarity index 97% rename from ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html rename to ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html index 2f27726d90..daf70c19e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html @@ -40,11 +40,11 @@
- + datakey.label -
- + datakey.units - + datakey.decimals diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss rename to ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss index e5f6c02ef7..2ef3488e53 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../../../scss/constants'; +@import '../../../../../../scss/constants'; :host { .tb-datakey-config { diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts similarity index 95% rename from ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts rename to ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts index 2fbcc1e5ef..9d9201e88a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts @@ -40,7 +40,7 @@ import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; -import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { Observable, of } from 'rxjs'; import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; @@ -52,6 +52,7 @@ import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models'; import { genNextLabel } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-data-key-config', @@ -90,6 +91,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con comparisonResultTypeTranslations = comparisonResultTypeTranslationMap; + @Input() + deviceId: string; + @Input() entityAliasId: string; @@ -117,6 +121,22 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @Input() showPostProcessing = true; + @Input() + @coerceBoolean() + hideDataKeyLabel = false; + + @Input() + @coerceBoolean() + hideDataKeyColor = false; + + @Input() + @coerceBoolean() + hideDataKeyUnits = false; + + @Input() + @coerceBoolean() + hideDataKeyDecimals = false; + @ViewChild('keyInput') keyInput: ElementRef; @ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent; @@ -377,9 +397,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con if (this.modelValue.type === DataKeyType.alarm) { fetchObservable = of(this.alarmKeys); } else { - if (this.entityAliasId) { + if (this.deviceId || this.entityAliasId) { const dataKeyTypes = [this.modelValue.type]; - fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + if (this.deviceId) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } } else { fetchObservable = of([]); } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html similarity index 79% rename from ui-ngx/src/app/modules/home/components/widget/data-keys.component.html rename to ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html index 0230c4ae08..39fffd79e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html @@ -15,85 +15,92 @@ limitations under the License. --> - - + + {{placeholder}} +
- - +
+
-
-
- drag_handle -
-
-
+
+ drag_indicator
- - + notifications - - - timeline - {{key.label}} + {{key.label}}
-
:
+
:
+
+
+
+
+
+ - close
- @@ -105,7 +112,7 @@ [displayWith]="displayKeyFn"> - + notifications @@ -132,7 +139,7 @@ {{ translate.get('entity.no-key-matching', {key: truncate.transform(searchText, true, 6, '...')}) | async }} - + entity.create-new-key @@ -162,7 +169,7 @@ {{ requiredText }} -
+
{{ maxDataKeysText() }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts similarity index 90% rename from ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts rename to ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts index 9407145376..a5519be953 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts @@ -21,4 +21,5 @@ import { Observable } from 'rxjs'; export interface DataKeysCallbacks { generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey; fetchEntityKeys: (entityAliasId: string, types: Array) => Observable>; + fetchEntityKeysForDevice: (deviceId: string, types: Array) => Observable>; } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss similarity index 86% rename from ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss rename to ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss index a0b909af1a..3013bf78ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss @@ -28,6 +28,13 @@ line-height: 20px; height: 32px; + &.mdc-evolution-chip--with-trailing-action { + .mdc-evolution-chip__action--primary { + padding-left: 4px; + padding-right: 12px; + } + } + .mat-mdc-chip-action { overflow: hidden; .mat-mdc-chip-action-label { @@ -37,15 +44,12 @@ .tb-attribute-chip { max-width: 100%; color: rgb(66, 66, 66); - font-weight: normal; - font-size: 16px; .tb-chip-drag-handle { + padding: 3px; + height: 24px; cursor: move; mat-icon { pointer-events: none; - margin-right: 4px; - margin-left: 4px; - vertical-align: bottom; } } .tb-chip-labels { @@ -53,7 +57,13 @@ flex-direction: row; align-items: center; min-width: 0; + background: rgba(0, 0, 0, 0.04); + border-radius: 100px; + padding: 2px 10px; .tb-chip-label { + font-weight: normal; + font-size: 14px; + line-height: 20px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -70,15 +80,9 @@ white-space: pre; } } - .mat-mdc-chip-remove.mat-icon { - width: 24px; - min-width: 24px; - height: 24px; - font-size: 24px; - margin-right: 4px; + .mat-mdc-chip-remove.mat-mdc-icon-button { color: inherit; opacity: inherit; - padding-left: 0; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts similarity index 72% rename from ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts rename to ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts index 23051d78ad..95fe5bc444 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts @@ -30,6 +30,7 @@ import { ViewEncapsulation } from '@angular/core'; import { + AbstractControl, ControlValueAccessor, FormGroupDirective, NG_VALUE_ACCESSOR, @@ -37,14 +38,14 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - Validators + ValidationErrors } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { MatAutocomplete } from '@angular/material/autocomplete'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatChipGrid, MatChipInputEvent, MatChipRow } from '@angular/material/chips'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -60,12 +61,14 @@ import { MatDialog } from '@angular/material/dialog'; import { DataKeyConfigDialogComponent, DataKeyConfigDialogData -} from '@home/components/widget/data-key-config-dialog.component'; -import { deepClone, guid, isUndefined } from '@core/utils'; +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone, guid, isDefinedAndNotNull, isUndefined } from '@core/utils'; import { Dashboard } from '@shared/models/dashboard.models'; import { AggregationType } from '@shared/models/time/time.models'; import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive'; import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; @Component({ selector: 'tb-data-keys', @@ -76,16 +79,32 @@ import { moveItemInArray } from '@angular/cdk/drag-drop'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DataKeysComponent), multi: true - }, + } /*, { provide: ErrorStateMatcher, useExisting: DataKeysComponent - } + } */ ], encapsulation: ViewEncapsulation.None }) export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChanges, ErrorStateMatcher { + public get hideDataKeyLabel(): boolean { + return this.datasourceComponent.hideDataKeyLabel; + } + + public get hideDataKeyColor(): boolean { + return this.datasourceComponent.hideDataKeyColor; + } + + public get hideDataKeyUnits(): boolean { + return this.datasourceComponent.hideDataKeyUnits; + } + + public get hideDataKeyDecimals(): boolean { + return this.datasourceComponent.hideDataKeyDecimals; + } + datasourceTypes = DatasourceType; widgetTypes = widgetType; dataKeyTypes = DataKeyType; @@ -113,6 +132,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange @Input() optDataKeys: boolean; + @Input() + @coerceBoolean() + simpleDataKeysLabel = false; + @Input() aliasController: IAliasController; @@ -134,6 +157,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange @Input() entityAliasId: string; + @Input() + deviceId: string; + private requiredValue: boolean; get required(): boolean { return this.requiredValue || !this.optDataKeys || this.isCountDatasource; @@ -148,6 +174,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange @ViewChild('keyInput') keyInput: ElementRef; @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; @ViewChild('chipList') chipList: MatChipGrid; keys: Array = []; @@ -178,6 +205,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange constructor(private store: Store, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private datasourceComponent: DatasourceComponent, public translate: TranslateService, private utils: UtilsService, private dialogs: DialogService, @@ -189,10 +217,19 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } updateValidators() { - this.keysListFormGroup.get('keys').setValidators(this.required ? [Validators.required] : []); + this.keysListFormGroup.get('keys').setValidators(this.required ? [this.keysRequired] : []); this.keysListFormGroup.get('keys').updateValueAndValidity(); } + keysRequired(control: AbstractControl): ValidationErrors | null { + const value = control.value; + if (value && Array.isArray(value) && value.length) { + return null; + } else { + return {required: true}; + } + } + registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -202,7 +239,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange ngOnInit() { this.keysListFormGroup = this.fb.group({ - keys: [null, this.required ? [Validators.required] : []], + keys: [null, this.required ? [this.keysRequired] : []], key: [null] }); this.alarmKeys = []; @@ -251,37 +288,40 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } private updateParams() { - if (this.datasourceType === DatasourceType.function) { - this.dataKeyType = DataKeyType.function; - this.requiredText = this.translate.instant('datakey.function-types-required'); - if (this.widgetType === widgetType.latest) { - this.placeholder = this.translate.instant('datakey.latest-key-functions'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key-function'); - } else if (this.widgetType === widgetType.alarm) { - this.placeholder = this.translate.instant('datakey.alarm-key-functions'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key-function'); - } else { - this.placeholder = this.translate.instant('datakey.timeseries-key-functions'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key-function'); - } + const singleKey = this.maxDataKeysSet && this.maxDataKeys === 1; + this.secondaryPlaceholder = '+' + this.translate.instant('action.add'); + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + this.requiredText = this.translate.instant('datakey.function-types-required'); + if (this.widgetType === widgetType.latest) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key-function' : 'datakey.latest-key-functions'); + } else if (this.widgetType === widgetType.alarm) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key-function' : 'datakey.alarm-key-functions'); + } else { + this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key-function' : 'datakey.timeseries-key-functions'); + } + } else { + if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) { + this.dataKeyType = DataKeyType.timeseries; + } else { + this.dataKeyType = null; + } + if (this.simpleDataKeysLabel && this.widgetType !== widgetType.alarm) { + this.placeholder = this.translate.instant(singleKey ? 'datakey.data-key' : 'datakey.data-keys'); + this.requiredText = this.translate.instant(singleKey ? 'datakey.data-key-required' : 'datakey.data-keys-required'); } else { if (this.widgetType === widgetType.latest) { - this.dataKeyType = null; - this.placeholder = this.translate.instant('datakey.latest-keys'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key'); + this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key' : 'datakey.latest-keys'); this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required'); } else if (this.widgetType === widgetType.alarm) { - this.dataKeyType = null; - this.placeholder = this.translate.instant('datakey.alarm-keys'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key'); + this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key' : 'datakey.alarm-keys'); this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required'); } else { - this.dataKeyType = DataKeyType.timeseries; - this.placeholder = this.translate.instant('datakey.timeseries-keys'); - this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key'); + this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key' : 'datakey.timeseries-keys'); this.requiredText = this.translate.instant('datakey.timeseries-required'); } } + } } private reset() { @@ -307,16 +347,22 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'entityAliasId') { + if (['deviceId', 'entityAliasId'].includes(propName)) { this.clearSearchCache(); this.dirty = true; - } else if (['widgetType', 'datasourceType'].includes(propName)) { - this.clearSearchCache(); - this.updateParams(); - setTimeout(() => { - this.reset(); - }, 1); - } else if (['required', 'optDataKeys'].includes('propName')) { + } else if (['widgetType', 'datasourceType', 'maxDataKeys', 'simpleDataKeysLabel'].includes(propName)) { + if (propName === 'datasourceType' && + [DatasourceType.device, DatasourceType.entity].includes(change.previousValue) && + [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) { + this.clearSearchCache(); + } else { + this.clearSearchCache(); + this.updateParams(); + setTimeout(() => { + this.reset(); + }, 1); + } + } else if (['required', 'optDataKeys'].includes(propName)) { this.updateValidators(); } } @@ -325,7 +371,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = this.required && !this.modelValue; + const customErrorState = this.required && (!this.modelValue || !this.modelValue.length); return originalErrorState || customErrorState; } @@ -365,7 +411,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } addKey(key: DataKey): void { - if (!this.maxDataKeys || this.maxDataKeys < 0 || + if (!this.maxDataKeysSet || !this.modelValue || this.modelValue.length < this.maxDataKeys) { if (!this.modelValue) { this.modelValue = []; @@ -375,17 +421,17 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange this.keysListFormGroup.get('keys').setValue(this.keys); } this.propagateChange(this.modelValue); - this.clear(); + const focus = !this.maxDataKeysSet || this.modelValue.length < this.maxDataKeys; + this.clear('', focus); } add(event: MatChipInputEvent): void { const value = event.value; - if ((value || '').trim()) { - if (this.dataKeyType) { - this.addFromChipValue({name: value.trim(), type: this.dataKeyType}); - } + if ((value || '').trim() && this.dataKeyType) { + this.addFromChipValue({name: value.trim(), type: this.dataKeyType}); + } else { + this.clear(); } - this.clear(); } remove(key: DataKey) { @@ -402,8 +448,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } } - chipDragStart(index: number, chipRow: MatChipRow, placeholderChipRow: MatChipRow) { - this.renderer.setStyle(placeholderChipRow._elementRef.nativeElement, 'width', chipRow._elementRef.nativeElement.offsetWidth + 'px'); + chipDragStart(index: number, chipRow: MatChipRow, placeholderChipRow: Element) { + this.autocomplete.closePanel(); + this.renderer.setStyle(placeholderChipRow, + 'width', chipRow._elementRef.nativeElement.offsetWidth + 'px'); this.dragIndex = index; } @@ -447,9 +495,14 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange aliasController: this.aliasController, widget: this.widget, widgetType: this.widgetType, + deviceId: this.deviceId, entityAliasId: this.entityAliasId, showPostProcessing: this.widgetType !== widgetType.alarm, - callbacks: this.callbacks + callbacks: this.callbacks, + hideDataKeyLabel: this.hideDataKeyLabel, + hideDataKeyColor: this.hideDataKeyColor, + hideDataKeyUnits: this.hideDataKeyUnits, + hideDataKeyDecimals: this.hideDataKeyDecimals } }).afterClosed().subscribe((updatedDataKey) => { if (updatedDataKey) { @@ -496,7 +549,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange if (this.datasourceType === DatasourceType.function) { const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; fetchObservable = of(targetKeysList); - } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId) { + } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || + this.datasourceType === DatasourceType.device && this.deviceId) { const dataKeyTypes = [DataKeyType.timeseries]; if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { dataKeyTypes.push(DataKeyType.attribute); @@ -505,7 +559,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange dataKeyTypes.push(DataKeyType.alarm); } } - fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + if (this.datasourceType === DatasourceType.device) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } } else { fetchObservable = of([]); } @@ -523,22 +581,41 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } textIsNotEmpty(text: string): boolean { - return (text && text != null && text.length > 0) ? true : false; + return text && text.length > 0; } - clear(value: string = '') { + clear(value: string = '', focus = true) { + this.autocomplete.closePanel(); this.keyInput.nativeElement.value = value; - this.keysListFormGroup.get('key').patchValue(value, {emitEvent: true}); - setTimeout(() => { - this.keyInput.nativeElement.blur(); - this.keyInput.nativeElement.focus(); - }, 0); + this.keysListFormGroup.get('key').patchValue(value, {emitEvent: focus}); + if (focus) { + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } } get isCountDatasource(): boolean { return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType); } + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + + get inputDisabled(): boolean { + return this.isCountDatasource || (this.maxDataKeysSet && this.keys.length >= this.maxDataKeys); + } + + get dragDisabled(): boolean { + return this.keys.length < 2; + } + + get maxDataKeysSet(): boolean { + return isDefinedAndNotNull(this.maxDataKeysValue) && this.maxDataKeysValue > -1; + } + private clearSearchCache() { this.searchText = ''; this.fetchObservable$ = null; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html new file mode 100644 index 0000000000..5cf519637b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -0,0 +1,103 @@ + +
+ + widget-config.datasource-type + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + + +
+ + + datasource.label + + + + + + + + + + + + + +
+
+ + + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts similarity index 65% rename from ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts rename to ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts index e8a7e24c63..0664058fb4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts @@ -14,9 +14,8 @@ /// limitations under the License. /// -import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; -import { DataKeysCallbacks } from './data-keys.component.models'; -import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models'; +import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; -export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks; +export type DatasourceCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss new file mode 100644 index 0000000000..163e74d336 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-datasource-section { + display: flex; + flex-direction: column; + align-items: stretch; + flex: 1; +} + +:host ::ng-deep { + .tb-datasource-section { + tb-alarm-filter-config { + .mdc-button { + width: 100%; + height: 100%; + justify-content: flex-start; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts new file mode 100644 index 0000000000..6fca263c53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts @@ -0,0 +1,275 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, Optional } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + Validators +} from '@angular/forms'; +import { + Datasource, + DatasourceType, + datasourceTypeTranslationMap, + JsonSettingsSchema, + Widget, WidgetConfigMode, + widgetType +} from '@shared/models/widget.models'; +import { AlarmSearchStatus } from '@shared/models/alarm.models'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { IAliasController } from '@core/api/widget-api.models'; +import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; +import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; + +@Component({ + selector: 'tb-datasource', + templateUrl: './datasource.component.html', + styleUrls: ['./datasource.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourceComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DatasourceComponent), + multi: true, + } + ] +}) +export class DatasourceComponent implements ControlValueAccessor, OnInit, Validator { + + public get basicMode(): boolean { + return !this.widgetConfigComponent.widgetEditMode && this.widgetConfigComponent.widgetConfigMode === WidgetConfigMode.basic; + } + + public get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + public get aliasController(): IAliasController { + return this.widgetConfigComponent.aliasController; + } + + public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + public get filterSelectCallbacks(): FilterSelectCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + public get dataKeysCallbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + public get hasAdditionalLatestDataKeys(): boolean { + return this.widgetConfigComponent.widgetType === widgetType.timeseries && + this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys; + } + + public get dataKeysOptional(): boolean { + return this.widgetConfigComponent.modelValue?.typeParameters?.dataKeysOptional; + } + + public get maxDataKeys(): number { + return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys; + } + + public get dataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + public get dataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; + } + + public get latestDataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema; + } + + public get latestDataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective; + } + + public get dashboard(): Dashboard { + return this.widgetConfigComponent.dashboard; + } + + public get widget(): Widget { + return this.widgetConfigComponent.widget; + } + + public get hideDataKeyLabel(): boolean { + return this.datasourcesComponent?.hideDataKeyLabel; + } + + public get hideDataKeyColor(): boolean { + return this.datasourcesComponent?.hideDataKeyColor; + } + + public get hideDataKeyUnits(): boolean { + return this.datasourcesComponent?.hideDataKeyUnits; + } + + public get hideDataKeyDecimals(): boolean { + return this.datasourcesComponent?.hideDataKeyDecimals; + } + + @Input() + disabled: boolean; + + widgetTypes = widgetType; + + entityType = EntityType; + + datasourceType = DatasourceType; + datasourceTypes: Array = []; + datasourceTypesTranslations = datasourceTypeTranslationMap; + + datasourceFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + @Optional() + private datasourcesComponent: DatasourcesComponent, + private widgetConfigComponent: WidgetConfigComponent) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (!this.datasourceFormGroup.valid) { + setTimeout(() => { + this.datasourceUpdated(this.datasourceFormGroup.value); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.datasourceFormGroup.disable({emitEvent: false}); + } else { + this.datasourceFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + ngOnInit() { + if (this.widgetConfigComponent.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; + if (this.widgetConfigComponent.widgetType === widgetType.latest) { + this.datasourceTypes.push(DatasourceType.entityCount); + this.datasourceTypes.push(DatasourceType.alarmCount); + } + } + + this.datasourceFormGroup = this.fb.group( + { + type: [null, [Validators.required]], + name: [null, []], + deviceId: [null, []], + entityAliasId: [null, []], + filterId: [null, []], + dataKeys: [null, []], + alarmFilterConfig: [null, []] + } + ); + if (this.hasAdditionalLatestDataKeys) { + this.datasourceFormGroup.addControl('latestDataKeys', this.fb.control(null)); + } + this.datasourceFormGroup.get('type').valueChanges.subscribe(() => { + this.updateValidators(); + }); + this.datasourceFormGroup.valueChanges.subscribe( + () => { + this.datasourceUpdated(this.datasourceFormGroup.value); + } + ); + } + + writeValue(datasource?: Datasource): void { + this.datasourceFormGroup.patchValue({ + type: datasource?.type, + name: datasource?.name, + deviceId: datasource?.deviceId, + entityAliasId: datasource?.entityAliasId, + filterId: datasource?.filterId, + dataKeys: datasource?.dataKeys, + alarmFilterConfig: datasource?.alarmFilterConfig ? + datasource?.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE] } + }, {emitEvent: false}); + if (this.hasAdditionalLatestDataKeys) { + this.datasourceFormGroup.patchValue({ + latestDataKeys: datasource?.latestDataKeys + }, {emitEvent: false}); + } + this.updateValidators(); + } + + validate(c: UntypedFormControl) { + return (this.datasourceFormGroup.valid) ? null : { + datasource: { + valid: false, + }, + }; + } + + public isDataKeysOptional(type?: DatasourceType): boolean { + if (this.hasAdditionalLatestDataKeys) { + return true; + } else { + return this.dataKeysOptional + && type !== DatasourceType.entityCount && type !== DatasourceType.alarmCount; + } + } + + private datasourceUpdated(datasource: Datasource) { + this.propagateChange(datasource); + } + + private updateValidators() { + const type: DatasourceType = this.datasourceFormGroup.get('type').value; + this.datasourceFormGroup.get('deviceId').setValidators( + type === DatasourceType.device ? [Validators.required] : [] + ); + this.datasourceFormGroup.get('entityAliasId').setValidators( + (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] + ); + const newDataKeysRequired = !this.isDataKeysOptional(type); + this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []); + this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false}); + this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false}); + this.datasourceFormGroup.get('dataKeys').updateValueAndValidity({emitEvent: false}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html new file mode 100644 index 0000000000..a5a8a89004 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html @@ -0,0 +1,101 @@ + +
+
+
+
{{ (singleDatasource ? 'widget-config.datasource' : 'widget-config.datasources') | translate }}
+
+ {{ 'widget-config.timeseries-key-error' | translate }} +
+ + +
+
{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}
+
+
+ datasource.add-datasource-prompt +
+ +
+ + +
+
+
{{$index + 1}}
+
+
+
+ + +
+ + +
+
+ +
+
+
+
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss new file mode 100644 index 0000000000..5b356afcf2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../theme"; + +.tb-datasource-list-item { + &.mat-mdc-list-item { + height: auto; + display: block; + padding: 0; + &.bordered { + padding-top: 16px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + } + } + &.tb-draggable { + &.cdk-drag-preview { + background: #fff; + } + } +} + +.tb-datasource-list-item + .tb-datasource-list-item { + margin-top: 16px; +} + +.tb-datasource-index { + width: 24px; + height: 24px; + position: relative; + font-weight: 400; + font-size: 16px; + text-align: center; + color: $tb-primary-color; + &:before { + content: ""; + display: block; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: $tb-primary-color; + opacity: 0.06; + border-radius: 100%; + } +} + +.tb-datasource-params { + position: relative; + tb-error.tb-datasource-error { + position: absolute; + bottom: 4px; + left: 8px; + } +} + +:host { + .tb-datasources { + + .handle { + cursor: move; + } + + .mat-mdc-list { + min-height: 68px; + padding-left: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts new file mode 100644 index 0000000000..383b9e15ee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts @@ -0,0 +1,329 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { + Datasource, + DatasourceType, + JsonSettingsSchema, + WidgetConfigMode, + widgetType +} from '@shared/models/widget.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { deepClone } from '@core/utils'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-datasources', + templateUrl: './datasources.component.html', + styleUrls: ['./datasources.component.scss', './widget-config.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourcesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DatasourcesComponent), + multi: true, + } + ] +}) +export class DatasourcesComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { + + datasourceType = DatasourceType; + + public get basicMode(): boolean { + return !this.widgetConfigComponent.widgetEditMode && this.configMode === WidgetConfigMode.basic; + } + + public get maxDatasources(): number { + return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources; + } + + public get singleDatasource(): boolean { + return this.maxDatasources === 1; + } + + public get showAddDatasource(): boolean { + return this.widgetConfigComponent.modelValue?.typeParameters && + (this.maxDatasources === -1 || this.datasourcesFormArray.length < this.maxDatasources); + } + + public get dragDisabled(): boolean { + return this.disabled || this.singleDatasource || this.datasourcesFormArray.length < 2; + } + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + hideDataKeyLabel = false; + + @Input() + @coerceBoolean() + hideDataKeyColor = false; + + @Input() + @coerceBoolean() + hideDataKeyUnits = false; + + @Input() + @coerceBoolean() + hideDataKeyDecimals = false; + + @Input() + configMode: WidgetConfigMode; + + datasourcesFormGroup: UntypedFormGroup; + + timeseriesKeyError = false; + + datasourceError: string[] = []; + + datasourcesMode: DatasourceType; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private utils: UtilsService, + public translate: TranslateService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.validate(null)) { + setTimeout(() => { + this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.datasourcesFormGroup.disable({emitEvent: false}); + } else { + this.datasourcesFormGroup.enable({emitEvent: false}); + } + } + + ngOnInit() { + this.datasourcesFormGroup = this.fb.group({ + datasources: this.fb.array([]) + }); + this.datasourcesFormGroup.valueChanges.subscribe( + () => { + this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); + } + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'configMode') { + this.configModeChanged(); + } + } + } + } + + writeValue(datasources?: Datasource[]): void { + this.datasourcesFormArray.clear({emitEvent: false}); + this.datasourcesMode = this.detectDatasourcesMode(datasources); + let changed = false; + if (datasources) { + datasources.forEach((datasource) => { + if (this.basicMode && datasource.type !== this.datasourcesMode) { + datasource.type = this.datasourcesMode; + changed = true; + } + this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false}); + }); + } + if (this.singleDatasource && !this.datasourcesFormArray.length) { + this.addDatasource(false); + } + if (changed) { + setTimeout(() => { + this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); + }, 0); + } + } + + validate(c: UntypedFormControl) { + this.timeseriesKeyError = false; + this.datasourceError = []; + if (!this.datasourcesFormGroup.valid) { + return { + datasources: { + valid: false, + } + }; + } + const datasources: Datasource[] = this.datasourcesFormGroup.get('datasources').value; + if (!this.datasourcesOptional && (!datasources || !datasources.length)) { + return { + datasources: { + valid: false + } + }; + } + if (this.hasAdditionalLatestDataKeys) { + let valid = datasources.filter(datasource => datasource?.dataKeys?.length).length > 0; + if (!valid) { + this.timeseriesKeyError = true; + return { + timeseriesDataKeys: { + valid: false + } + }; + } else { + const emptyDatasources = datasources.filter(datasource => !datasource?.dataKeys?.length && + !datasource?.latestDataKeys?.length); + valid = emptyDatasources.length === 0; + if (!valid) { + for (const emptyDatasource of emptyDatasources) { + const i = datasources.indexOf(emptyDatasource); + this.datasourceError[i] = 'At least one data key should be specified'; + } + return { + dataKeys: { + valid: false + } + }; + } + } + } + return null; + } + + datasourcesModeChange(datasourcesMode: DatasourceType) { + this.datasourcesMode = datasourcesMode; + if (this.basicMode) { + for (const datasourceControl of this.datasourcesControls) { + const datasource: Datasource = datasourceControl.value; + if (datasource.type !== datasourcesMode) { + datasource.type = datasourcesMode; + datasourceControl.patchValue(datasource); + } + } + } + } + + private configModeChanged() { + if (this.basicMode) { + let datasourcesMode = this.detectDatasourcesMode(this.datasourcesFormGroup.get('datasources').value); + this.datasourcesModeChange(datasourcesMode); + } + } + + private detectDatasourcesMode(datasources?: Datasource[]) { + let datasourcesMode = DatasourceType.device; + if (datasources && datasources.length) { + datasourcesMode = datasources[0].type; + } + if (datasourcesMode !== DatasourceType.device && datasourcesMode !== DatasourceType.entity) { + datasourcesMode = DatasourceType.device; + } + return datasourcesMode; + } + + get datasourcesFormArray(): UntypedFormArray { + return this.datasourcesFormGroup.get('datasources') as UntypedFormArray; + } + + get datasourcesControls(): FormControl[] { + return this.datasourcesFormArray.controls as FormControl[]; + } + + public trackByDatasource(index: number, datasourceControl: AbstractControl): any { + return datasourceControl; + } + + private datasourcesUpdated(datasources: Datasource[]) { + this.propagateChange(datasources); + } + + public onDatasourceDrop(event: CdkDragDrop) { + const datasourceForm = this.datasourcesFormArray.at(event.previousIndex); + this.datasourcesFormArray.removeAt(event.previousIndex); + this.datasourcesFormArray.insert(event.currentIndex, datasourceForm); + } + + public removeDatasource(index: number) { + this.datasourcesFormArray.removeAt(index); + } + + public addDatasource(emitEvent = true) { + let newDatasource: Datasource; + if (this.widgetConfigComponent.functionsOnly) { + newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); + newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)]; + } else { + const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity; + newDatasource = { type, + dataKeys: [] + }; + } + if (this.hasAdditionalLatestDataKeys) { + newDatasource.latestDataKeys = []; + } + this.datasourcesFormArray.push(this.fb.control(newDatasource, []), {emitEvent}); + } + + private get dataKeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + private get dataKeysCallbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + private get hasAdditionalLatestDataKeys(): boolean { + return this.widgetConfigComponent.widgetType === widgetType.timeseries && + this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys; + } + + private get datasourcesOptional(): boolean { + return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html new file mode 100644 index 0000000000..985db7d942 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html @@ -0,0 +1,42 @@ + +
+
+
timewindow.timewindow
+ + +
+
+ + + {{ 'widget-config.display-timewindow' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts new file mode 100644 index 0000000000..ec7e8d0854 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { widgetType } from '@shared/models/widget.models'; +import { Timewindow } from '@shared/models/time/time.models'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +export interface TimewindowConfigData { + useDashboardTimewindow: boolean; + displayTimewindow: boolean; + timewindow: Timewindow; +} + +@Component({ + selector: 'tb-timewindow-config-panel', + templateUrl: './timewindow-config-panel.component.html', + styleUrls: ['./widget-config.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimewindowConfigPanelComponent), + multi: true + } + ] +}) +export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnInit { + + widgetTypes = widgetType; + + public get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + onlyHistoryTimewindow = false; + + + timewindowConfig: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + public translate: TranslateService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.timewindowConfig = this.fb.group({ + useDashboardTimewindow: [null, []], + displayTimewindow: [null, []], + timewindow: [null, []] + }); + this.timewindowConfig.valueChanges.subscribe( + (val) => this.propagateChange(val) + ); + this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => { + this.updateTimewindowConfigEnabledState(); + }); + } + + writeValue(data?: TimewindowConfigData): void { + this.timewindowConfig.patchValue(data || {}, {emitEvent: false}); + this.updateTimewindowConfigEnabledState(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.timewindowConfig.disable({emitEvent: false}); + } else { + this.timewindowConfig.enable({emitEvent: false}); + this.updateTimewindowConfigEnabledState(); + } + } + + private updateTimewindowConfigEnabledState() { + const useDashboardTimewindow: boolean = this.timewindowConfig.get('useDashboardTimewindow').value; + if (useDashboardTimewindow) { + this.timewindowConfig.get('displayTimewindow').disable({emitEvent: false}); + this.timewindowConfig.get('timewindow').disable({emitEvent: false}); + } else { + this.timewindowConfig.get('displayTimewindow').enable({emitEvent: false}); + this.timewindowConfig.get('timewindow').enable({emitEvent: false}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts new file mode 100644 index 0000000000..186a89537e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component'; +import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; +import { DataKeysComponent } from '@home/components/widget/config/data-keys.component'; +import { DataKeyConfigDialogComponent } from '@home/components/widget/config/data-key-config-dialog.component'; +import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; +import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; +import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; +import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component'; +import { FilterSelectComponent } from '@home/components/filter/filter-select.component'; +import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; +import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component'; +import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component'; +import { WidgetUnitsComponent } from '@home/components/widget/config/widget-units.component'; + +@NgModule({ + declarations: + [ + AlarmAssigneeSelectComponent, + AlarmFilterConfigComponent, + DataKeysComponent, + DataKeyConfigDialogComponent, + DataKeyConfigComponent, + DatasourceComponent, + DatasourcesComponent, + EntityAliasSelectComponent, + FilterSelectComponent, + TimewindowConfigPanelComponent, + WidgetUnitsComponent, + WidgetSettingsComponent + ], + imports: [ + CommonModule, + SharedModule, + WidgetSettingsModule + ], + exports: [ + AlarmAssigneeSelectComponent, + AlarmFilterConfigComponent, + DataKeysComponent, + DataKeyConfigDialogComponent, + DataKeyConfigComponent, + DatasourceComponent, + DatasourcesComponent, + EntityAliasSelectComponent, + FilterSelectComponent, + TimewindowConfigPanelComponent, + WidgetUnitsComponent, + WidgetSettingsComponent + ] +}) +export class WidgetConfigComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts new file mode 100644 index 0000000000..39b3c2a741 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { DatasourceCallbacks } from '@home/components/widget/config/datasource.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, UntypedFormGroup } from '@angular/forms'; +import { WidgetConfigMode } from '@shared/models/widget.models'; + +export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; + +export interface IBasicWidgetConfigComponent { + + widgetConfig: WidgetConfigComponentData; + widgetConfigChanged: Observable; + validateConfig(): boolean; + +} + +@Directive() +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export abstract class BasicWidgetConfigComponent extends PageComponent implements + IBasicWidgetConfigComponent, OnInit, AfterViewInit { + + basicMode = WidgetConfigMode.basic; + + widgetConfigValue: WidgetConfigComponentData; + + set widgetConfig(value: WidgetConfigComponentData) { + this.widgetConfigValue = value; + this.setupConfig(this.widgetConfigValue); + } + + get widgetConfig(): WidgetConfigComponentData { + return this.widgetConfigValue; + } + + widgetConfigChangedEmitter = new EventEmitter(); + widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); + + protected constructor(@Inject(Store) protected store: Store) { + super(store); + } + + ngOnInit() {} + + ngAfterViewInit(): void { + setTimeout(() => { + if (!this.validateConfig()) { + this.onConfigChanged(this.prepareOutputConfig(this.configForm().value)); + } + }, 0); + } + + protected setupConfig(widgetConfig: WidgetConfigComponentData) { + this.onConfigSet(widgetConfig); + this.updateValidators(false); + for (const trigger of this.validatorTriggers()) { + const path = trigger.split('.'); + let control: AbstractControl = this.configForm(); + for (const part of path) { + control = control.get(part); + } + control.valueChanges.subscribe(() => { + this.updateValidators(true, trigger); + }); + } + this.configForm().valueChanges.subscribe((updated: any) => { + this.onConfigChanged(this.prepareOutputConfig(updated)); + }); + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + } + + protected validatorTriggers(): string[] { + return []; + } + + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + this.widgetConfigValue = widgetConfig; + this.widgetConfigChangedEmitter.emit(this.widgetConfigValue); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + return config; + } + + public validateConfig(): boolean { + return this.configForm().valid; + } + + protected abstract configForm(): UntypedFormGroup; + + protected abstract onConfigSet(configData: WidgetConfigComponentData); + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss new file mode 100644 index 0000000000..15e14e211e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss @@ -0,0 +1,169 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-widget-config-panel { + box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); + border-radius: 4px; + padding: 16px; + gap: 16px; + display: flex; + flex-direction: column; + color: rgba(0, 0, 0, 0.87); + letter-spacing: 0.15px; + position: relative; + &.no-padding-bottom { + padding-bottom: 0; + } + &.stroked { + box-shadow: none; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + } +} +.tb-widget-config-panel-title { + font-weight: 500; + font-size: 16px; +} +.tb-widget-config-panel-hint { + font-size: 12px; + color: #808080; +} +.tb-widget-config-row { + height: 56px; + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + padding-left: 16px; + padding-right: 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + &.same-padding { + padding-right: 16px; + } + &.space-between { + justify-content: space-between; + } + .mat-divider-vertical { + height: 56px; + } +} + +:host ::ng-deep { + + .mat-slide { + margin: 8px 0; + .mdc-form-field>label { + font-weight: 400; + font-size: 16px; + line-height: 24px; + margin-left: 12px; + } + } + + .slide-block { + display: block; + &:not(:last-child) { + margin-bottom: 8px; + } + } + + .mat-mdc-form-field { + &.center { + .mat-mdc-text-field-wrapper.mdc-text-field--outlined { + .mat-mdc-form-field-infix { + .mdc-text-field__input { + text-align: center; + } + } + } + } + &.number { + .mat-mdc-text-field-wrapper.mdc-text-field--outlined { + padding-right: 4px; + .mat-mdc-form-field-infix { + width: 80px; + input.mdc-text-field__input[type=number]::-webkit-inner-spin-button, + input.mdc-text-field__input[type=number]::-webkit-outer-spin-button { + opacity: 1; + } + } + } + } + } + + .tb-widget-config-row { + .mat-mdc-form-field { + .mat-mdc-text-field-wrapper.mdc-text-field--outlined { + padding-right: 12px; + padding-left: 12px; + &:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) { + .mdc-notched-outline__leading, .mdc-notched-outline__trailing { + border-color: rgba(0, 0, 0, 0.12); + } + } + .mat-mdc-form-field-infix { + padding-top: 7px; + padding-bottom: 7px; + min-height: 38px; + width: 72px; + } + } + } + } + + .tb-widget-config-panel { + .mat-expansion-panel { + &.tb-settings { + box-shadow: none; + .mat-content { + overflow: visible; + } + .mat-expansion-panel-header { + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.25px; + padding: 0; + .mat-content { + flex: 0; + white-space: nowrap; + } + &:hover { + background: none; + } + .mat-expansion-indicator { + height: 32px; + padding: 2px; + } + } + .mat-expansion-panel-header-description { + align-items: center; + } + > .mat-expansion-panel-content { + > .mat-expansion-panel-body { + padding: 0; + } + } + .tb-json-object-panel, .tb-css-content-panel { + margin: 0 0 8px; + } + } + .mat-expansion-panel-content { + font: inherit; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss rename to ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts similarity index 91% rename from ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts index 7bfb66fd61..cc482bfd4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts @@ -16,28 +16,27 @@ import { AfterViewInit, - Component, ComponentFactoryResolver, + Component, + ComponentFactoryResolver, ComponentRef, forwardRef, - Input, OnChanges, + Input, + OnChanges, OnDestroy, - OnInit, SimpleChanges, + OnInit, + SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { - IRuleNodeConfigurationComponent, - RuleNodeConfiguration, - RuleNodeDefinition -} from '@shared/models/rule-node.models'; + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { Subscription } from 'rxjs'; -import { RuleChainService } from '@core/http/rule-chain.service'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { TranslateService } from '@ngx-translate/core'; -import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; -import { deepClone } from '@core/utils'; -import { RuleChainType } from '@shared/models/rule-chain.models'; import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models'; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html new file mode 100644 index 0000000000..d573e65183 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html @@ -0,0 +1,20 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts new file mode 100644 index 0000000000..4a1665b5b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms'; + +@Component({ + selector: 'tb-widget-units', + templateUrl: './widget-units.component.html', + styleUrls: ['./widget-config.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetUnitsComponent), + multi: true + } + ] +}) +export class WidgetUnitsComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + unitsFormControl: FormControl; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.unitsFormControl = this.fb.control('', []); + } + + writeValue(units?: string): void { + this.unitsFormControl.patchValue(units, {emitEvent: false}); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.unitsFormControl.disable({emitEvent: false}); + } else { + this.unitsFormControl.enable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.html new file mode 100644 index 0000000000..336e137ad6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.html @@ -0,0 +1,32 @@ + +
+ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts new file mode 100644 index 0000000000..9272295a27 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts @@ -0,0 +1,151 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { ChartType, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; +import { TbFlot } from '@home/components/widget/lib/flot-widget'; +import { + defaultLegendConfig, + LegendConfig, + LegendData, + LegendPosition, + widgetType +} from '@shared/models/widget.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +@Component({ + selector: 'tb-flot-widget', + templateUrl: './flot-widget.component.html', + styleUrls: [] +}) +export class FlotWidgetComponent implements OnInit { + + @ViewChild('flotElement', {static: true}) flotElement: ElementRef; + + @Input() + ctx: WidgetContext; + + @Input() + chartType: ChartType; + + displayLegend: boolean; + legendConfig: LegendConfig; + legendData: LegendData; + isLegendFirst: boolean; + legendContainerLayoutType: string; + legendStyle: {[klass: string]: any}; + + public settings: TbFlotSettings; + private flot: TbFlot; + + constructor() { + } + + ngOnInit(): void { + this.ctx.$scope.flotWidget = this; + this.settings = this.ctx.settings; + this.chartType = this.chartType || 'line'; + this.configureLegend(); + this.flot = new TbFlot(this.ctx, this.chartType, $(this.flotElement.nativeElement)); + } + + private configureLegend(): void { + + this.displayLegend = isDefinedAndNotNull(this.settings.showLegend) ? this.settings.showLegend + : false; + + this.legendContainerLayoutType = 'column'; + + if (this.displayLegend) { + this.legendConfig = this.settings.legendConfig || defaultLegendConfig(widgetType.timeseries); + if (this.ctx.defaultSubscription) { + this.legendData = this.ctx.defaultSubscription.legendData; + } else { + this.legendData = { + keys: [], + data: [] + }; + } + if (this.legendConfig.position === LegendPosition.top || + this.legendConfig.position === LegendPosition.bottom) { + this.legendContainerLayoutType = 'column'; + this.isLegendFirst = this.legendConfig.position === LegendPosition.top; + } else { + this.legendContainerLayoutType = 'row'; + this.isLegendFirst = this.legendConfig.position === LegendPosition.left; + } + switch (this.legendConfig.position) { + case LegendPosition.top: + this.legendStyle = { + paddingBottom: '8px', + maxHeight: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.bottom: + this.legendStyle = { + paddingTop: '8px', + maxHeight: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.left: + this.legendStyle = { + paddingRight: '0px', + maxWidth: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.right: + this.legendStyle = { + paddingLeft: '0px', + maxWidth: '50%', + overflowY: 'auto' + }; + break; + } + } + } + + public onLegendKeyHiddenChange(index: number) { + for (const id of Object.keys(this.ctx.subscriptions)) { + const subscription = this.ctx.subscriptions[id]; + subscription.updateDataVisibility(index); + } + } + + public onDataUpdated() { + this.flot.update(); + } + + public onLatestDataUpdated() { + this.flot.latestDataUpdate(); + } + + public onResize() { + this.flot.resize(); + } + + public onEditModeChanged() { + this.flot.checkMouseEvents(); + } + + public onDestroy() { + this.flot.destroy(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts index 6e9e7f85fa..2cc6b40300 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -17,13 +17,21 @@ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -import { DataKey, Datasource, DatasourceData, FormattedData, JsonSettingsSchema } from '@shared/models/widget.models'; +import { + DataKey, + Datasource, + DatasourceData, + FormattedData, + JsonSettingsSchema, + LegendConfig +} from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { ComparisonDuration } from '@shared/models/time/time.models'; export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph'; -export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings; +export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotLegendSettings & + TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings; export declare type TooltipValueFormatFunction = (value: any, latestData: FormattedData) => string; @@ -140,6 +148,11 @@ export interface TbFlotBaseSettings { yaxis: TbFlotYAxisSettings; } +export interface TbFlotLegendSettings { + showLegend?: boolean; + legendConfig?: LegendConfig; +} + export interface TbFlotComparisonSettings { comparisonEnabled: boolean; timeForComparison: ComparisonDuration; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index 08a8c92472..c347fa52fc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -127,7 +127,7 @@ export class TbFlot { private pieAnimationLastTime: number; private pieAnimationCaf: CancelAnimationFrame; - constructor(private ctx: WidgetContext, private readonly chartType: ChartType) { + constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery) { this.chartType = this.chartType || 'line'; this.settings = ctx.settings as TbFlotSettings; this.utils = this.ctx.$injector.get(UtilsService); @@ -334,7 +334,7 @@ export class TbFlot { } if (this.ctx.defaultSubscription) { - this.init(this.ctx.$container, this.ctx.defaultSubscription); + this.init(this.$flotElement || this.ctx.$container, this.ctx.defaultSubscription); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts index 84aeef4118..1b69de8c45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts @@ -28,7 +28,6 @@ import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home- import { GettingStartedCompletedDialogComponent } from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; -import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/toggle-header.component'; import { UsageInfoWidgetComponent } from '@home/components/widget/lib/home-page/usage-info-widget.component'; import { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component'; import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component'; @@ -49,7 +48,6 @@ import { EditLinksDialogComponent, GettingStartedWidgetComponent, GettingStartedCompletedDialogComponent, - ToggleHeaderComponent, UsageInfoWidgetComponent, QuickLinksWidgetComponent, QuickLinkComponent, @@ -70,7 +68,6 @@ import { EditLinksDialogComponent, GettingStartedWidgetComponent, GettingStartedCompletedDialogComponent, - ToggleHeaderComponent, UsageInfoWidgetComponent, QuickLinksWidgetComponent, QuickLinkComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/legend.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/legend.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/legend.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html index 8b60b19007..1ee182f923 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html @@ -15,16 +15,19 @@ limitations under the License. --> -
- - widgets.simple-card.label-position - - - {{ 'widgets.simple-card.label-position-left' | translate }} - - - {{ 'widgets.simple-card.label-position-top' | translate }} - - - -
+
+
widgets.simple-card.label
+
+
widgets.simple-card.label-position
+ + + + {{ 'widgets.simple-card.label-position-left' | translate }} + + + {{ 'widgets.simple-card.label-position-top' | translate }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts index 4ecd21ad1d..b9080b93ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-simple-card-widget-settings', templateUrl: './simple-card-widget-settings.component.html', - styleUrls: [] + styleUrls: ['../../../config/widget-config.scss'] }) export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html index fd68f51a14..5422c034f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html @@ -68,6 +68,25 @@ +
+ widget-config.legend + + + + + {{ 'widget-config.display-legend' | translate }} + + + + widget-config.advanced-settings + + + + + + +
widgets.chart.tooltip-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index e243232437..98c4acd62b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -40,8 +40,9 @@ import { labelDataKeyValidator } from '@home/components/widget/lib/settings/chart/label-data-key.component'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { defaultLegendConfig, widgetType } from '@shared/models/widget.models'; -export function flotDefaultSettings(chartType: ChartType): Partial { +export const flotDefaultSettings = (chartType: ChartType): Partial => { const settings: Partial = { stack: false, fontColor: '#545454', @@ -95,9 +96,11 @@ export function flotDefaultSettings(chartType: ChartType): Partial { + this.updateValidators(true); + }); this.flotSettingsFormGroup.get('comparisonEnabled').valueChanges.subscribe(() => { this.updateValidators(true); }); @@ -350,9 +361,15 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit this.flotSettingsFormGroup.get('yaxis.ticksFormatter').updateValueAndValidity({emitEvent: false}); if (this.chartType === 'graph' || this.chartType === 'bar') { + const showLegend: boolean = this.flotSettingsFormGroup.get('showLegend').value; const comparisonEnabled: boolean = this.flotSettingsFormGroup.get('comparisonEnabled').value; const timeForComparison: ComparisonDuration = this.flotSettingsFormGroup.get('timeForComparison').value; const customLegendEnabled: boolean = this.flotSettingsFormGroup.get('customLegendEnabled').value; + if (showLegend) { + this.flotSettingsFormGroup.get('legendConfig').enable({emitEvent}); + } else { + this.flotSettingsFormGroup.get('legendConfig').disable({emitEvent}); + } if (comparisonEnabled) { this.flotSettingsFormGroup.get('timeForComparison').enable({emitEvent: false}); if (timeForComparison === 'customInterval') { @@ -370,6 +387,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit this.flotSettingsFormGroup.get('dataKeysListForLabels').disable({emitEvent}); } + this.flotSettingsFormGroup.get('legendConfig').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/legend-config.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 11aaf06a93..2dae32a922 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -265,6 +265,7 @@ import { import { QuickLinksWidgetSettingsComponent } from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component'; +import { LegendConfigComponent } from '@home/components/widget/lib/settings/common/legend-config.component'; @NgModule({ declarations: [ @@ -290,6 +291,7 @@ import { AnalogueCompassWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent, ValueSourceComponent, + LegendConfigComponent, FixedColorLevelComponent, TickValueComponent, FlotWidgetSettingsComponent, @@ -395,6 +397,7 @@ import { AnalogueCompassWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent, ValueSourceComponent, + LegendConfigComponent, FixedColorLevelComponent, TickValueComponent, FlotWidgetSettingsComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 9b73b5566d..e3ff270197 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -99,6 +99,8 @@ export class WidgetComponentService { settingsDirective: this.utils.editWidgetInfo.settingsDirective, dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective, latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective, + hasBasicMode: this.utils.editWidgetInfo.hasBasicMode, + basicModeDirective: this.utils.editWidgetInfo.basicModeDirective, defaultConfig: this.utils.editWidgetInfo.defaultConfig }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined ); @@ -162,7 +164,7 @@ export class WidgetComponentService { (window as any).TbMapWidgetV2 = mod.TbMapWidgetV2; })) ); - widgetModulesTasks.push(from(import('@home/components/widget/trip-animation/trip-animation.component')).pipe( + widgetModulesTasks.push(from(import('@home/components/widget/lib/trip-animation/trip-animation.component')).pipe( tap((mod) => { (window as any).TbTripAnimationWidget = mod.TbTripAnimationWidget; })) @@ -404,6 +406,9 @@ export class WidgetComponentService { if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) { directives.push(widgetInfo.latestDataKeySettingsDirective); } + if (widgetInfo.basicModeDirective && widgetInfo.basicModeDirective.length) { + directives.push(widgetInfo.basicModeDirective); + } if (directives.length) { factories.filter((factory) => directives.includes(factory.selector)) .forEach((foundFactory) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 4f0454bb1c..ce2e7055cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -29,7 +29,7 @@ import { DateRangeNavigatorWidgetComponent } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component'; import { MultipleInputWidgetComponent } from '@home/components/widget/lib/multiple-input-widget.component'; -import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component'; +import { TripAnimationComponent } from '@home/components/widget/lib/trip-animation/trip-animation.component'; import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component'; import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; @@ -41,6 +41,8 @@ import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-wi import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module'; import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; +import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component'; +import { LegendComponent } from '@home/components/widget/lib/legend.component'; @NgModule({ declarations: @@ -62,7 +64,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; NavigationCardWidgetComponent, QrCodeWidgetComponent, MarkdownWidgetComponent, - SelectEntityDialogComponent + SelectEntityDialogComponent, + LegendComponent, + FlotWidgetComponent ], imports: [ CommonModule, @@ -88,7 +92,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; NavigationCardsWidgetComponent, NavigationCardWidgetComponent, QrCodeWidgetComponent, - MarkdownWidgetComponent + MarkdownWidgetComponent, + LegendComponent, + FlotWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 7d39455f36..1766b79009 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -15,480 +15,268 @@ limitations under the License. --> - - -
-
-
- - {{ 'widget-config.use-dashboard-timewindow' | translate }} - - - {{ 'widget-config.display-timewindow' | translate }} - +
+
+ + + + +
+
+
+ +
+
+ +
+
+
+
widget-config.card-title
+ + {{ 'widget-config.display-title' | translate }} + +
+ + widget-config.title + + + + widget-config.title-tooltip + +
-
- widget-config.timewindow - -
-
-
- -
- - - - -
-
widget-config.datasources
-
{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}
-
- - report_gmailerrorred - -
- - {{ 'widget-config.timeseries-key-error' | translate }} +
+ + {{ 'widget-config.display-icon' | translate }} + +
+ + + + + + + + +
+
+ + + + widget-config.advanced-title-style -
- datasource.add-datasource-prompt -
- -
- -
- widget-config.datasource-type - widget-config.datasource-parameters - -
-
-
- - -
-
- - {{$index + 1}}. -
-
-
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - datasource.label - - - - - - - - - - - - - -
-
- - - - -
-
- -
- -
-
-
-
- -
+ + -
- -
- - - - {{ 'widget-config.target-device' | translate }} - - -
- - +
+
+
widget-config.card-style
+
+
{{ 'widget-config.text' | translate }}
+
+ + +
- - - - - {{ 'widget-config.alarm-source' | translate }} - - -
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - - - - -
- - -
+
+
+
{{ 'widget-config.background' | translate }}
+
+ + +
- - +
+
+
{{ 'widget-config.padding' | translate }}
+ + + +
+
+
{{ 'widget-config.margin' | translate }}
+ + + +
+ + {{ 'widget-config.drop-shadow' | translate }} + + - widget-config.data-settings + + widget-config.advanced-widget-style + -
- - widget-config.data-page-size - - -
-
- - widget-config.units - - - - widget-config.decimals - - -
-
- - widget-config.no-data-display-message - - -
+ +
- +
+
+
widget-config.card-buttons
+ + {{ 'widget-config.enable-fullscreen' | translate }} + +
- - -
-
-
- widget-config.title - - {{ 'widget-config.display-title' | translate }} - -
- - widget-config.title - - - - widget-config.title-tooltip - - -
-
- widget-config.title-icon - - {{ 'widget-config.display-icon' | translate }} - -
- - - - - - widget-config.icon-size - - -
-
- - - - widget-config.advanced-settings - - - - - - -
-
- widget-config.widget-style -
-
- - - - -
-
- - widget-config.padding - - - - widget-config.margin - - -
-
- - {{ 'widget-config.drop-shadow' | translate }} - - - {{ 'widget-config.enable-fullscreen' | translate }} - - - - - widget-config.advanced-settings - - - - - - - -
-
- widget-config.legend - - - - - {{ 'widget-config.display-legend' | translate }} - - - - widget-config.advanced-settings - - - - - - -
-
- widget-config.mobile-mode-settings - - - - - {{ 'widget-config.mobile-hide' | translate }} - - - {{ 'widget-config.desktop-hide' | translate }} - - - - widget-config.advanced-settings - - - -
- - widget-config.order - - - - widget-config.height - - -
-
-
-
+
+
+ +
- - -
- - +
+
+ + {{ 'widget-config.mobile-hide' | translate }} + +
+
+ + {{ 'widget-config.desktop-hide' | translate }} + +
+
+
+
widget-config.order
+ + + +
+
+
widget-config.height
+ + + +
+
+
+
+ +
+
+ +
{{basicModeDirectiveError}}
+
+ +
+ + + + + + +
+
-
- - - - - + + + + + + +
+ +
+
+ + +
+
+
widget-config.target-device
+
+ + +
+
+
+
widget-config.alarm-source
+ + +
+
+
widget-config.limits
+
+
widget-config.data-page-size
+ + + +
+
+
+ +
+
widget-config.data-settings
+
+
widget-config.units
+ + +
+
+
widget-config.decimals
+ + + +
+
+
widget-config.no-data-display-message
+ + + +
+
+
+ + +
+
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 2ae2eaa211..91b3368ad0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -13,118 +13,46 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +@import "../../../../../theme"; @import '../../../../../scss/constants'; .tb-widget-config { - .tb-panel-hint { - font-size: 12px; - color: #808080; - } -} - -.tb-datasource-list-item { - &.mat-mdc-list-item { - height: auto; - min-height: 68px; - display: block; - padding: 0; - } - &.tb-draggable { - &.cdk-drag-preview { - background: #fff; - } - } -} - -.tb-datasource-params { - position: relative; - padding: 0 0 0 10px; - margin: 5px; - tb-error.tb-datasource-error { - position: absolute; - bottom: 4px; - left: 8px; - } - .tb-datasource-section { + display: flex; + flex-direction: column; + gap: 16px; + .tb-widget-config-header { + padding: 24px 24px 8px; + height: 56px; display: flex; - flex-direction: column; - align-items: stretch; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + .tb-widget-config-content { flex: 1; - padding-top: 20px; - @media #{$mat-gt-sm} { - flex-direction: row; - align-items: center; - justify-content: flex-start; + overflow: auto; + & > div { + padding: 16px; } - } - .tb-datasource-type { - min-width: 160px; - @media #{$mat-gt-sm} { - max-width: 160px; + .mat-content { + display: flex; + flex-direction: column; + gap: 16px; } } - .tb-datasource { - @media #{$mat-gt-sm} { - padding-left: 8px; - width: 208px; - max-width: 208px; - } - } - .tb-data-keys { - @media #{$mat-gt-sm} { - padding-left: 8px; - } - } -} - -:host { - .tb-widget-config { - .tb-advanced-widget-config { - height: 100%; - } - .tb-datasources { - - .handle { - cursor: move; - } - - .mat-mdc-list { - min-height: 68px; - padding-left: 0; - } - } - .fields-group { - padding: 0 16px 8px; - margin-bottom: 10px; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; - legend { - color: rgba(0, 0, 0, .7); - width: fit-content; - } - } - .fields-group-slider { - padding: 0; - legend { - margin-left: 16px; - } - .tb-settings { - padding: 0 16px 8px; - } - } - .tb-widget-style { - margin-top: 16px; - } + .tb-basic-mode-directive-error { + font-size: 13px; + font-weight: 400; + color: rgb(221, 44, 0); } } :host ::ng-deep { .tb-widget-config { - tb-alarm-filter-config { - .mdc-button { - width: 100%; - height: 100%; - justify-content: flex-start; + .tb-widget-config-header { + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + padding: 0 20px; } } .mat-mdc-tab-body-wrapper { @@ -134,53 +62,5 @@ right: 0; bottom: 0; } - .mat-expansion-panel { - &.tb-settings { - box-shadow: none; - .mat-content { - overflow: visible; - } - .mat-expansion-panel-header { - padding: 0; - &:hover { - background: none; - } - .mat-expansion-indicator { - padding: 2px; - } - } - .mat-expansion-panel-header-description { - align-items: center; - } - > .mat-expansion-panel-content { - > .mat-expansion-panel-body { - padding: 0; - } - } - .tb-json-object-panel, .tb-css-content-panel { - margin: 0 0 8px; - } - } - &.tb-datasources { - &.mat-expanded { - overflow: visible; - } - .mat-expansion-panel-body{ - padding: 0 12px 16px; - } - } - .mat-expansion-panel-content { - font: inherit; - } - } - .mat-slide { - margin: 8px 0; - } - .slide-block { - display: block; - &:not(:last-child) { - margin-bottom: 8px; - } - } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 1514c1b929..744747a50a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -14,29 +14,38 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + ComponentRef, + forwardRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DataKey, - Datasource, datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, - DatasourceType, - datasourceTypeTranslationMap, - defaultLegendConfig, GroupInfo, JsonSchema, JsonSettingsSchema, Widget, + WidgetConfigMode, widgetType } from '@shared/models/widget.models'; import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, @@ -53,7 +62,10 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { Observable, of, Subscription } from 'rxjs'; -import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models'; +import { + IBasicWidgetConfigComponent, + WidgetConfigCallbacks +} from '@home/components/widget/config/widget-config.component.models'; import { EntityAliasDialogComponent, EntityAliasDialogData @@ -62,12 +74,15 @@ import { catchError, mergeMap, tap } from 'rxjs/operators'; import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; -import { WidgetActionsData } from './action/manage-widget-actions.component.models'; import { Dashboard } from '@shared/models/dashboard.models'; import { entityFields } from '@shared/models/entity.models'; -import { Filter } from '@shared/models/query/query.models'; +import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; +import Timeout = NodeJS.Timeout; +import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -81,7 +96,7 @@ const defaultSettingsForm = [ @Component({ selector: 'tb-widget-config', templateUrl: './widget-config.component.html', - styleUrls: ['./widget-config.component.scss'], + styleUrls: ['./widget-config.component.scss', './config/widget-config.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -95,10 +110,14 @@ const defaultSettingsForm = [ } ] }) -export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { +export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator, OnChanges { + + @ViewChild('basicModeContainer', {read: ViewContainerRef, static: false}) basicModeContainer: ViewContainerRef; widgetTypes = widgetType; + widgetConfigModes = WidgetConfigMode; + entityTypes = EntityType; @Input() @@ -116,65 +135,76 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @Input() functionsOnly: boolean; + @Input() + @coerceBoolean() + hideHeader = false; + + @Input() + @coerceBoolean() + hideToggleHeader = false; + @Input() disabled: boolean; - widgetType: widgetType; + @Input() + widgetConfigMode = WidgetConfigMode.advanced; - datasourceType = DatasourceType; - datasourceTypes: Array = []; - datasourceTypesTranslations = datasourceTypeTranslationMap; + widgetType: widgetType; widgetConfigCallbacks: WidgetConfigCallbacks = { createEntityAlias: this.createEntityAlias.bind(this), createFilter: this.createFilter.bind(this), generateDataKey: this.generateDataKey.bind(this), + fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this), fetchEntityKeys: this.fetchEntityKeys.bind(this), fetchDashboardStates: this.fetchDashboardStates.bind(this) }; widgetEditMode = this.utils.widgetEditMode; - selectedTab: number; + basicModeDirectiveError: string; modelValue: WidgetConfigComponentData; - showLegendFieldset = true; - private propagateChange = null; + headerOptions: ToggleHeaderOption[] = []; + selectedOption: string; + public dataSettings: UntypedFormGroup; public targetDeviceSettings: UntypedFormGroup; - public alarmSourceSettings: UntypedFormGroup; public widgetSettings: UntypedFormGroup; public layoutSettings: UntypedFormGroup; public advancedSettings: UntypedFormGroup; public actionsSettings: UntypedFormGroup; - public openExtensionPanel = true; - public timeseriesKeyError = false; - public datasourceError: string[] = []; + private createBasicModeComponentTimeout: Timeout; + private basicModeComponentRef: ComponentRef; + private basicModeComponent: IBasicWidgetConfigComponent; + private basicModeComponentChangeSubscription: Subscription; private dataSettingsChangesSubscription: Subscription; private targetDeviceSettingsSubscription: Subscription; - private alarmSourceSettingsSubscription: Subscription; private widgetSettingsSubscription: Subscription; private layoutSettingsSubscription: Subscription; private advancedSettingsSubscription: Subscription; private actionsSettingsSubscription: Subscription; + private defaultConfigFormsType: widgetType; + constructor(protected store: Store, private utils: UtilsService, private entityService: EntityService, private dialog: MatDialog, - private translate: TranslateService, - private fb: UntypedFormBuilder) { + public translate: TranslateService, + private cfr: ComponentFactoryResolver, + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { super(store); } ngOnInit(): void { this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); - this.alarmSourceSettings = this.fb.group({}); this.advancedSettings = this.fb.group({}); this.widgetSettings = this.fb.group({ title: [null, []], @@ -196,41 +226,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont pageSize: [1024, [Validators.min(1), Validators.pattern(/^\d*$/)]], units: [null, []], decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], - noDataDisplayMessage: [null, []], - showLegend: [null, []], - legendConfig: [null, []] - }); - this.widgetSettings.get('showTitle').valueChanges.subscribe((value: boolean) => { - if (value) { - this.widgetSettings.get('titleStyle').enable({emitEvent: false}); - this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleStyle').disable({emitEvent: false}); - this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').patchValue(false); - this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); - } + noDataDisplayMessage: [null, []] }); - this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { - if (value) { - this.widgetSettings.get('titleIcon').enable({emitEvent: false}); - this.widgetSettings.get('iconColor').enable({emitEvent: false}); - this.widgetSettings.get('iconSize').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleIcon').disable({emitEvent: false}); - this.widgetSettings.get('iconColor').disable({emitEvent: false}); - this.widgetSettings.get('iconSize').disable({emitEvent: false}); - } + this.widgetSettings.get('showTitle').valueChanges.subscribe(() => { + this.updateWidgetSettingsEnabledState(); }); - this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { - if (value) { - this.widgetSettings.get('legendConfig').enable({emitEvent: false}); - } else { - this.widgetSettings.get('legendConfig').disable({emitEvent: false}); - } + this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => { + this.updateWidgetSettingsEnabledState(); }); + this.layoutSettings = this.fb.group({ mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]], @@ -238,11 +243,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont desktopHide: [false] }); this.actionsSettings = this.fb.group({ - actionsData: [null, []] + actions: [null, []] }); } + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'widgetConfigMode') { + if (this.hasBasicModeDirective) { + this.setupConfig(); + } + } + } + } + } + ngOnDestroy(): void { + this.destroyBasicModeComponent(); this.removeChangeSubscriptions(); } @@ -255,10 +274,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.targetDeviceSettingsSubscription.unsubscribe(); this.targetDeviceSettingsSubscription = null; } - if (this.alarmSourceSettingsSubscription) { - this.alarmSourceSettingsSubscription.unsubscribe(); - this.alarmSourceSettingsSubscription = null; - } if (this.widgetSettingsSubscription) { this.widgetSettingsSubscription.unsubscribe(); this.widgetSettingsSubscription = null; @@ -284,9 +299,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( () => this.updateTargetDeviceSettings() ); - this.alarmSourceSettingsSubscription = this.alarmSourceSettings.valueChanges.subscribe( - () => this.updateAlarmSourceSettings() - ); this.widgetSettingsSubscription = this.widgetSettings.valueChanges.subscribe( () => this.updateWidgetSettings() ); @@ -301,34 +313,57 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont ); } - private buildForms() { - if (this.functionsOnly) { - this.datasourceTypes = [DatasourceType.function]; - } else { - this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; - if (this.widgetType === widgetType.latest) { - this.datasourceTypes.push(DatasourceType.entityCount); - this.datasourceTypes.push(DatasourceType.alarmCount); + private buildHeader() { + this.headerOptions.length = 0; + if (this.widgetType !== widgetType.static) { + this.headerOptions.push( + { + name: this.translate.instant('widget-config.data'), + value: 'data' + } + ); + } + if (this.displayAppearance) { + this.headerOptions.push( + { + name: this.translate.instant('widget-config.appearance'), + value: 'appearance' + } + ); + } + this.headerOptions.push( + { + name: this.translate.instant('widget-config.widget-card'), + value: 'card' + } + ); + this.headerOptions.push( + { + name: this.translate.instant('widget-config.actions'), + value: 'actions' + } + ); + this.headerOptions.push( + { + name: this.translate.instant('widget-config.mobile'), + value: 'mobile' } + ); + if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) { + this.selectedOption = this.headerOptions[0].value; } + } + private buildForms() { this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); - this.alarmSourceSettings = this.fb.group({}); this.advancedSettings = this.fb.group({}); if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { - this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true)); - this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true})); - this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true})); - this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => { - if (value) { - this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); - this.dataSettings.get('timewindow').disable({emitEvent: false}); - } else { - this.dataSettings.get('displayTimewindow').enable({emitEvent: false}); - this.dataSettings.get('timewindow').enable({emitEvent: false}); - } - }); + this.dataSettings.addControl('timewindowConfig', this.fb.control({ + useDashboardTimewindow: true, + displayTimewindow: true, + timewindow: null + })); if (this.widgetType === widgetType.alarm) { this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); } @@ -337,24 +372,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont if (this.widgetType !== widgetType.rpc && this.widgetType !== widgetType.alarm && this.widgetType !== widgetType.static) { - this.dataSettings.addControl('datasources', - this.fb.array([])); + this.dataSettings.addControl('datasources', this.fb.control(null)); } else if (this.widgetType === widgetType.rpc) { this.targetDeviceSettings.addControl('targetDeviceAliasId', this.fb.control(null, this.widgetEditMode ? [] : [Validators.required])); } else if (this.widgetType === widgetType.alarm) { - this.alarmSourceSettings = this.buildDatasourceForm(); + this.dataSettings.addControl('alarmSource', this.fb.control(null)); } } this.advancedSettings.addControl('settings', this.fb.control(null, [])); } - datasourcesFormArray(): UntypedFormArray { - return this.dataSettings.get('datasources') as UntypedFormArray; - } - registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -368,213 +398,199 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont writeValue(value: WidgetConfigComponentData): void { this.modelValue = value; - this.removeChangeSubscriptions(); + this.widgetType = this.modelValue?.widgetType; + this.setupConfig(); + } + + private setupConfig() { if (this.modelValue) { - if (this.widgetType !== this.modelValue.widgetType) { - this.widgetType = this.modelValue.widgetType; - this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); - this.buildForms(); + this.destroyBasicModeComponent(); + this.removeChangeSubscriptions(); + if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) { + this.setupBasicModeConfig(); + } else { + this.setupDefaultConfig(); } - const config = this.modelValue.config; - const layout = this.modelValue.layout; - if (config) { - this.selectedTab = 0; - const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false; - this.widgetSettings.patchValue({ - title: config.title, - showTitleIcon: isDefined(config.showTitleIcon) && displayWidgetTitle ? config.showTitleIcon : false, - titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', - iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', - iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', - titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', - showTitle: displayWidgetTitle, - dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, - enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, - backgroundColor: config.backgroundColor, - color: config.color, - padding: config.padding, - margin: config.margin, - widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, - widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '', - titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { - fontSize: '16px', - fontWeight: 400 - }, - pageSize: isDefined(config.pageSize) ? config.pageSize : 1024, - units: config.units, - decimals: config.decimals, - noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '', - showLegend: isDefined(config.showLegend) ? config.showLegend : - this.widgetType === widgetType.timeseries, - legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType) - }, - {emitEvent: false} - ); - const showTitle: boolean = this.widgetSettings.get('showTitle').value; - if (showTitle) { - this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); - this.widgetSettings.get('titleStyle').enable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); - this.widgetSettings.get('titleStyle').disable({emitEvent: false}); - this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); - } - const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; - if (showTitleIcon) { - this.widgetSettings.get('titleIcon').enable({emitEvent: false}); - this.widgetSettings.get('iconColor').enable({emitEvent: false}); - this.widgetSettings.get('iconSize').enable({emitEvent: false}); - } else { - this.widgetSettings.get('titleIcon').disable({emitEvent: false}); - this.widgetSettings.get('iconColor').disable({emitEvent: false}); - this.widgetSettings.get('iconSize').disable({emitEvent: false}); - } - const showLegend: boolean = this.widgetSettings.get('showLegend').value; - if (showLegend) { - this.widgetSettings.get('legendConfig').enable({emitEvent: false}); - } else { - this.widgetSettings.get('legendConfig').disable({emitEvent: false}); - } - const actionsData: WidgetActionsData = { - actionsMap: config.actions || {}, - actionSources: this.modelValue.actionSources || {} - }; - this.actionsSettings.patchValue( - { - actionsData + } + } + + private setupBasicModeConfig() { + const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective]; + if (!componentType) { + this.basicModeDirectiveError = this.translate.instant('widget-config.settings-component-not-found', + {selector: this.modelValue.basicModeDirective}); + } else { + const factory = this.cfr.resolveComponentFactory(componentType); + this.createBasicModeComponentTimeout = setTimeout(() => { + this.createBasicModeComponentTimeout = null; + this.basicModeComponentRef = this.basicModeContainer.createComponent(factory); + this.basicModeComponent = this.basicModeComponentRef.instance; + this.basicModeComponent.widgetConfig = this.modelValue; + this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => { + this.modelValue = data; + this.propagateChange(this.modelValue); + }); + this.cd.markForCheck(); + }, 0); + } + } + + private destroyBasicModeComponent() { + this.basicModeDirectiveError = null; + if (this.basicModeComponentChangeSubscription) { + this.basicModeComponentChangeSubscription.unsubscribe(); + this.basicModeComponentChangeSubscription = null; + } + if (this.createBasicModeComponentTimeout) { + clearTimeout(this.createBasicModeComponentTimeout); + this.createBasicModeComponentTimeout = null; + } + if (this.basicModeComponentRef) { + this.basicModeComponentRef.destroy(); + this.basicModeComponentRef = null; + this.basicModeComponent = null; + } + if (this.basicModeContainer) { + this.basicModeContainer.clear(); + } + } + + private setupDefaultConfig() { + if (this.defaultConfigFormsType !== this.widgetType) { + this.defaultConfigFormsType = this.widgetType; + this.buildForms(); + } + this.buildHeader(); + const config = this.modelValue.config; + const layout = this.modelValue.layout; + if (config) { + const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false; + this.widgetSettings.patchValue({ + title: config.title, + showTitleIcon: isDefined(config.showTitleIcon) && displayWidgetTitle ? config.showTitleIcon : false, + titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', + iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', + iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', + titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', + showTitle: displayWidgetTitle, + dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, + enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, + backgroundColor: config.backgroundColor, + color: config.color, + padding: config.padding, + margin: config.margin, + widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, + widgetCss: isDefined(config.widgetCss) ? config.widgetCss : '', + titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { + fontSize: '16px', + fontWeight: 400 }, - {emitEvent: false} - ); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { - const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? - config.useDashboardTimewindow : true; - this.dataSettings.patchValue( - { useDashboardTimewindow }, {emitEvent: false} - ); - if (useDashboardTimewindow) { - this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); - this.dataSettings.get('timewindow').disable({emitEvent: false}); - } else { - this.dataSettings.get('displayTimewindow').enable({emitEvent: false}); - this.dataSettings.get('timewindow').enable({emitEvent: false}); - } - this.dataSettings.patchValue( - { displayTimewindow: isDefined(config.displayTimewindow) ? - config.displayTimewindow : true }, {emitEvent: false} - ); - this.dataSettings.patchValue( - { timewindow: config.timewindow }, {emitEvent: false} - ); - } - if (this.modelValue.isDataEnabled) { - if (this.widgetType !== widgetType.rpc && - this.widgetType !== widgetType.alarm && - this.widgetType !== widgetType.static) { - const datasourcesFormArray = this.dataSettings.get('datasources') as UntypedFormArray; - datasourcesFormArray.clear(); - if (config.datasources) { - config.datasources.forEach((datasource) => { - datasourcesFormArray.push(this.buildDatasourceForm(datasource)); - }); - } - } else if (this.widgetType === widgetType.rpc) { - let targetDeviceAliasId: string; - if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { - const aliasId = config.targetDeviceAliasIds[0]; - const entityAliases = this.aliasController.getEntityAliases(); - if (entityAliases[aliasId]) { - targetDeviceAliasId = entityAliases[aliasId].id; - } else { - targetDeviceAliasId = null; - } + pageSize: isDefined(config.pageSize) ? config.pageSize : 1024, + units: config.units, + decimals: config.decimals, + noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '' + }, + {emitEvent: false} + ); + this.updateWidgetSettingsEnabledState(); + this.actionsSettings.patchValue( + { + actions: config.actions || {} + }, + {emitEvent: false} + ); + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { + const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? + config.useDashboardTimewindow : true; + this.dataSettings.get('timewindowConfig').patchValue({ + useDashboardTimewindow, + displayTimewindow: isDefined(config.displayTimewindow) ? + config.displayTimewindow : true, + timewindow: config.timewindow + }, {emitEvent: false}); + } + if (this.modelValue.isDataEnabled) { + if (this.widgetType !== widgetType.rpc && + this.widgetType !== widgetType.alarm && + this.widgetType !== widgetType.static) { + this.dataSettings.patchValue({ datasources: config.datasources}, + {emitEvent: false}); + } else if (this.widgetType === widgetType.rpc) { + let targetDeviceAliasId: string; + if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { + const aliasId = config.targetDeviceAliasIds[0]; + const entityAliases = this.aliasController.getEntityAliases(); + if (entityAliases[aliasId]) { + targetDeviceAliasId = entityAliases[aliasId].id; } else { targetDeviceAliasId = null; } - this.targetDeviceSettings.patchValue({ - targetDeviceAliasId - }, {emitEvent: false}); - } else if (this.widgetType === widgetType.alarm) { - this.dataSettings.patchValue( - { alarmFilterConfig: isDefined(config.alarmFilterConfig) ? - config.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true } }, {emitEvent: false} - ); - this.alarmSourceSettings.patchValue( - config.alarmSource, {emitEvent: false} - ); - const alarmSourceType: DatasourceType = this.alarmSourceSettings.get('type').value; - this.alarmSourceSettings.get('entityAliasId').setValidators( - alarmSourceType === DatasourceType.entity ? [Validators.required] : [] - ); - this.alarmSourceSettings.get('entityAliasId').updateValueAndValidity({emitEvent: false}); + } else { + targetDeviceAliasId = null; } - } - - this.updateSchemaForm(config.settings); - - if (layout) { - this.layoutSettings.patchValue( - { - mobileOrder: layout.mobileOrder, - mobileHeight: layout.mobileHeight, - mobileHide: layout.mobileHide, - desktopHide: layout.desktopHide - }, - {emitEvent: false} - ); - } else { - this.layoutSettings.patchValue( - { - mobileOrder: null, - mobileHeight: null, - mobileHide: false, - desktopHide: false - }, - {emitEvent: false} + this.targetDeviceSettings.patchValue({ + targetDeviceAliasId + }, {emitEvent: false}); + } else if (this.widgetType === widgetType.alarm) { + this.dataSettings.patchValue( + { alarmFilterConfig: isDefined(config.alarmFilterConfig) ? + config.alarmFilterConfig : + { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true }, + alarmSource: config.alarmSource }, {emitEvent: false} ); } } - this.createChangeSubscriptions(); + + this.updateSchemaForm(config.settings); + + if (layout) { + this.layoutSettings.patchValue( + { + mobileOrder: layout.mobileOrder, + mobileHeight: layout.mobileHeight, + mobileHide: layout.mobileHide, + desktopHide: layout.desktopHide + }, + {emitEvent: false} + ); + } else { + this.layoutSettings.patchValue( + { + mobileOrder: null, + mobileHeight: null, + mobileHide: false, + desktopHide: false + }, + {emitEvent: false} + ); + } } + this.createChangeSubscriptions(); } - public dataKeysOptional(type?: DatasourceType): boolean { - if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - return true; + private updateWidgetSettingsEnabledState() { + const showTitle: boolean = this.widgetSettings.get('showTitle').value; + const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; + if (showTitle) { + this.widgetSettings.get('title').enable({emitEvent: false}); + this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); + this.widgetSettings.get('titleStyle').enable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); } else { - return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional - && type !== DatasourceType.entityCount && type !== DatasourceType.alarmCount; + this.widgetSettings.get('title').disable({emitEvent: false}); + this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); + this.widgetSettings.get('titleStyle').disable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); } - } - - private buildDatasourceForm(datasource?: Datasource): UntypedFormGroup { - const dataKeysRequired = !this.dataKeysOptional(datasource?.type); - const datasourceFormGroup = this.fb.group( - { - type: [datasource ? datasource.type : null, [Validators.required]], - name: [datasource ? datasource.name : null, []], - entityAliasId: [datasource ? datasource.entityAliasId : null, - datasource && (datasource.type === DatasourceType.entity || - datasource.type === DatasourceType.entityCount) ? [Validators.required] : []], - filterId: [datasource ? datasource.filterId : null, []], - dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []], - alarmFilterConfig: [datasource && datasource.alarmFilterConfig ? - datasource.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE] }] - } - ); - if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - datasourceFormGroup.addControl('latestDataKeys', this.fb.control(datasource ? datasource.latestDataKeys : null)); + if (showTitle && showTitleIcon) { + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); + this.widgetSettings.get('iconColor').enable({emitEvent: false}); + this.widgetSettings.get('iconSize').enable({emitEvent: false}); + } else { + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); + this.widgetSettings.get('iconColor').disable({emitEvent: false}); + this.widgetSettings.get('iconSize').disable({emitEvent: false}); } - datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => { - datasourceFormGroup.get('entityAliasId').setValidators( - (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] - ); - const newDataKeysRequired = !this.dataKeysOptional(type); - datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []); - datasourceFormGroup.get('entityAliasId').updateValueAndValidity(); - datasourceFormGroup.get('dataKeys').updateValueAndValidity(); - }); - return datasourceFormGroup; } private updateSchemaForm(settings?: any) { @@ -597,7 +613,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont private updateDataSettings() { if (this.modelValue) { if (this.modelValue.config) { - Object.assign(this.modelValue.config, this.dataSettings.value); + let data = this.dataSettings.value; + if (data.timewindowConfig) { + const timewindowConfig: TimewindowConfigData = data.timewindowConfig; + data = {...data, ...timewindowConfig}; + delete data.timewindowConfig; + } + Object.assign(this.modelValue.config, data); } this.propagateChange(this.modelValue); } @@ -617,16 +639,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } } - private updateAlarmSourceSettings() { - if (this.modelValue) { - if (this.modelValue.config) { - const alarmSource: Datasource = this.alarmSourceSettings.value; - this.modelValue.config.alarmSource = alarmSource; - } - this.propagateChange(this.modelValue); - } - } - private updateWidgetSettings() { if (this.modelValue) { if (this.modelValue.config) { @@ -648,8 +660,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont private updateAdvancedSettings() { if (this.modelValue) { if (this.modelValue.config) { - const settings = this.advancedSettings.get('settings').value?.model; - this.modelValue.config.settings = settings; + this.modelValue.config.settings = this.advancedSettings.get('settings').value?.model; } this.propagateChange(this.modelValue); } @@ -658,19 +669,30 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont private updateActionSettings() { if (this.modelValue) { if (this.modelValue.config) { - const actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap; - this.modelValue.config.actions = actions; + this.modelValue.config.actions = this.actionsSettings.get('actions').value; } this.propagateChange(this.modelValue); } } - public displayAdvanced(): boolean { + public get hasBasicModeDirective(): boolean { + return this.modelValue?.basicModeDirective?.length > 0; + } + + public get useDefinedBasicModeDirective(): boolean { + return this.modelValue?.basicModeDirective?.length && !this.basicModeDirectiveError; + } + + public get displayAppearance(): boolean { + return this.displayAppearanceDataSettings || this.displayAdvancedAppearance; + } + + public get displayAdvancedAppearance(): boolean { return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema || !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); } - public displayTimewindowConfig(): boolean { + public get displayTimewindowConfig(): boolean { if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { return true; } else if (this.widgetType === widgetType.latest) { @@ -679,40 +701,30 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } } - public onlyHistoryTimewindow(): boolean { - if (this.widgetType === widgetType.latest) { - const datasources = this.dataSettings.get('datasources').value; - return datasourcesHasOnlyComparisonAggregation(datasources); - } else { - return false; - } + public get displayLimits(): boolean { + return this.widgetType !== widgetType.rpc && this.widgetType !== widgetType.alarm && + this.modelValue?.isDataEnabled && !this.modelValue?.typeParameters?.singleEntity; + } + + public get displayAppearanceDataSettings(): boolean { + return this.displayUnitsConfig || this.displayNoDataDisplayMessageConfig; } - public onDatasourceDrop(event: CdkDragDrop) { - const datasourcesFormArray = this.datasourcesFormArray(); - const datasourceForm = datasourcesFormArray.at(event.previousIndex); - datasourcesFormArray.removeAt(event.previousIndex); - datasourcesFormArray.insert(event.currentIndex, datasourceForm); + public get displayUnitsConfig(): boolean { + return this.widgetType === widgetType.latest || this.widgetType === widgetType.timeseries; } - public removeDatasource(index: number) { - this.datasourcesFormArray().removeAt(index); + public get displayNoDataDisplayMessageConfig(): boolean { + return this.widgetType !== widgetType.static && !this.modelValue?.typeParameters?.processNoDataByWidget; } - public addDatasource() { - let newDatasource: Datasource; - if (this.functionsOnly) { - newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema)); - newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function, this.modelValue.dataKeySettingsSchema)]; + public onlyHistoryTimewindow(): boolean { + if (this.widgetType === widgetType.latest) { + const datasources = this.dataSettings.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); } else { - newDatasource = { type: DatasourceType.entity, - dataKeys: [] - }; - } - if (this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - newDatasource.latestDataKeys = []; + return false; } - this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource)); } public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey { @@ -810,17 +822,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont ); } + private fetchEntityKeysForDevice(deviceId: string, dataKeyTypes: Array): Observable> { + const entityFilter = singleEntityFilterFromDeviceId(deviceId); + return this.entityService.getEntityKeysByEntityFilter( + entityFilter, + dataKeyTypes, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + } + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { return this.aliasController.getAliasInfo(entityAliasId).pipe( - mergeMap((aliasInfo) => { - return this.entityService.getEntityKeysByEntityFilter( + mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, dataKeyTypes, {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) - ); - }), + )), catchError(() => of([] as Array)) ); } @@ -841,9 +862,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } public validate(c: UntypedFormControl) { - this.timeseriesKeyError = false; - this.datasourceError = []; - if (!this.dataSettings.valid) { + if (this.basicModeComponent) { + if (!this.basicModeComponent.validateConfig()) { + return { + basicWidgetConfig: { + valid: false + } + }; + } + } else if (!this.dataSettings.valid) { return { dataSettings: { valid: false @@ -861,7 +888,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont valid: false } }; - } else if (!this.advancedSettings.valid || (this.displayAdvanced() && !this.modelValue.config.settings)) { + } else if (!this.advancedSettings.valid || (this.displayAdvancedAppearance && !this.modelValue.config.settings)) { return { advancedSettings: { valid: false @@ -877,54 +904,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } }; } - } else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) { - if (!this.alarmSourceSettings.valid || !config.alarmSource) { - return { - alarmSource: { - valid: false - } - }; - } - } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) { - if (!this.modelValue.typeParameters.datasourcesOptional && (!config.datasources || !config.datasources.length)) { - return { - datasources: { - valid: false - } - }; - } - if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { - let valid = config.datasources.filter(datasource => datasource?.dataKeys?.length).length > 0; - if (!valid) { - this.timeseriesKeyError = true; - return { - timeseriesDataKeys: { - valid: false - } - }; - } else { - const emptyDatasources = config.datasources.filter(datasource => !datasource?.dataKeys?.length && - !datasource?.latestDataKeys?.length); - valid = emptyDatasources.length === 0; - if (!valid) { - for (const emptyDatasource of emptyDatasources) { - const i = config.datasources.indexOf(emptyDatasource); - this.datasourceError[i] = 'At least one data key should be specified'; - } - return { - dataKeys: { - valid: false - } - }; - } - } - } } } return null; } - - public extensionPanelIsOpen(value) { - this.openExtensionPanel = value; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html new file mode 100644 index 0000000000..870cb08513 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html @@ -0,0 +1,31 @@ + + + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss new file mode 100644 index 0000000000..1782251f61 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + z-index: 10; + background: #F3F6FA; + .tb-preview-dashboard { + position: absolute; + top: 15%; + bottom: 15%; + left: 0; + right: 0; + } + .tb-preview-panel { + position: absolute; + top: 16px; + left: 24px; + } +} + +:host ::ng-deep { + tb-dashboard.tb-preview-dashboard { + .tb-dashboard-content { + background-color: transparent !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts new file mode 100644 index 0000000000..cd088e5744 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts @@ -0,0 +1,79 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { IAliasController, IStateController } from '@core/api/widget-api.models'; +import { Widget, WidgetConfig } from '@shared/models/widget.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-widget-preview', + templateUrl: './widget-preview.component.html', + styleUrls: ['./widget-preview.component.scss'] +}) +export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges { + + @Input() + aliasController: IAliasController; + + @Input() + stateController: IStateController; + + @Input() + widget: Widget; + + @Input() + widgetConfig: WidgetConfig; + + widgets: Widget[]; + + constructor(protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.loadPreviewWidget(); + } + + ngOnChanges(changes: SimpleChanges): void { + let reloadPreviewWidget = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['widget', 'widgetConfig'].includes(propName)) { + reloadPreviewWidget = true; + } + } + } + if (reloadPreviewWidget) { + this.loadPreviewWidget(); + } + } + + private loadPreviewWidget() { + const widget = deepClone(this.widget); + widget.sizeX = 24; + widget.sizeY = this.widget.sizeY * 2; + widget.row = 0; + widget.col = 0; + widget.config = this.widgetConfig; + this.widgets = [widget]; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index ce5a5a57e7..cf287790a0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -15,23 +15,11 @@ limitations under the License. --> -
- -
- -
@@ -42,5 +30,5 @@ class="tb-absolute-fill">{{ noDataDisplayMessageText }}
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 8424999155..7e9b9cb6e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -39,10 +39,6 @@ import { } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { - defaultLegendConfig, - LegendConfig, - LegendData, - LegendPosition, Widget, WidgetActionDescriptor, widgetActionSources, @@ -152,13 +148,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI displayNoData = false; noDataDisplayMessageText: string; - displayLegend: boolean; - legendConfig: LegendConfig; - legendData: LegendData; - isLegendFirst: boolean; - legendContainerLayoutType: string; - legendStyle: {[klass: string]: any}; - dynamicWidgetComponentRef: ComponentRef; dynamicWidgetComponent: IDynamicWidgetComponent; @@ -220,57 +209,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widget = this.dashboardWidget.widget; - this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend - : this.widget.type === widgetType.timeseries; - - this.legendContainerLayoutType = 'column'; - - if (this.displayLegend) { - this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type); - this.legendData = { - keys: [], - data: [] - }; - if (this.legendConfig.position === LegendPosition.top || - this.legendConfig.position === LegendPosition.bottom) { - this.legendContainerLayoutType = 'column'; - this.isLegendFirst = this.legendConfig.position === LegendPosition.top; - } else { - this.legendContainerLayoutType = 'row'; - this.isLegendFirst = this.legendConfig.position === LegendPosition.left; - } - switch (this.legendConfig.position) { - case LegendPosition.top: - this.legendStyle = { - paddingBottom: '8px', - maxHeight: '50%', - overflowY: 'auto' - }; - break; - case LegendPosition.bottom: - this.legendStyle = { - paddingTop: '8px', - maxHeight: '50%', - overflowY: 'auto' - }; - break; - case LegendPosition.left: - this.legendStyle = { - paddingRight: '0px', - maxWidth: '50%', - overflowY: 'auto' - }; - break; - case LegendPosition.right: - this.legendStyle = { - paddingLeft: '0px', - maxWidth: '50%', - overflowY: 'auto' - }; - break; - } - } - const actionDescriptorsBySourceId: {[actionSourceId: string]: Array} = {}; if (this.widget.config.actions) { for (const actionSourceId of Object.keys(this.widget.config.actions)) { @@ -463,13 +401,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } } - public onLegendKeyHiddenChange(index: number) { - for (const id of Object.keys(this.widgetContext.subscriptions)) { - const subscription = this.widgetContext.subscriptions[id]; - subscription.updateDataVisibility(index); - } - } - private loadFromWidgetInfo() { this.widgetContext.widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; @@ -884,9 +815,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } this.createSubscription(options, subscribe).subscribe( (subscription) => { - if (useDefaultComponents) { - this.defaultSubscriptionOptions(subscription, options); - } createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, @@ -904,8 +832,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow; options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow; options.legendConfig = null; - if (this.displayLegend) { - options.legendConfig = this.legendConfig; + if (this.widget.config.settings.showLegend === true) { + options.legendConfig = this.widget.config.settings.legendConfig; } options.decimals = this.widgetContext.decimals; options.units = this.widgetContext.units; @@ -974,13 +902,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI }); } }; - - } - - private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) { - if (this.displayLegend) { - this.legendData = subscription.legendData; - } } private createDefaultSubscription(): Observable { @@ -1011,7 +932,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.createSubscription(options).subscribe( (subscription) => { - this.defaultSubscriptionOptions(subscription, options); // backward compatibility this.widgetContext.datasources = subscription.datasources; @@ -1491,7 +1411,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } } - private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, actionDescriptor: WidgetActionDescriptor): Observable { + private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array, + actionDescriptor: WidgetActionDescriptor): Observable { const resourceTasks: Observable[] = []; const modulesTasks: Observable[] = []; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 8ddb3d6218..c00d0a8e82 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -447,6 +447,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri } export interface WidgetConfigComponentData { + widgetName: string; config: WidgetConfig; layout: WidgetLayout; widgetType: widgetType; @@ -459,6 +460,7 @@ export interface WidgetConfigComponentData { settingsDirective: string; dataKeySettingsDirective: string; latestDataKeySettingsDirective: string; + basicModeDirective: string; } export const MissingWidgetType: WidgetInfo = { @@ -551,6 +553,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { settingsDirective: widgetTypeEntity.descriptor.settingsDirective, dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective, + hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode, + basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective, defaultConfig: widgetTypeEntity.descriptor.defaultConfig }; } @@ -581,6 +585,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: settingsDirective: widgetInfo.settingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, + hasBasicMode: widgetInfo.hasBasicMode, + basicModeDirective: widgetInfo.basicModeDirective, defaultConfig: widgetInfo.defaultConfig }; return { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index c4b859038e..046ad5acb2 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -270,6 +270,17 @@ [(ngModel)]="widget.latestDataKeySettingsDirective" (ngModelChange)="isDirty = true"/> + + {{ 'widget.has-basic-mode' | translate }} + + + widget.basic-mode-form-selector + +
diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index 542ec540da..492a2419eb 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + {{icon}} {{label}} @@ -34,3 +34,8 @@ {{ requiredText }} + +
+
+
+
diff --git a/ui-ngx/src/app/shared/components/color-input.component.scss b/ui-ngx/src/app/shared/components/color-input.component.scss index 0d5d861272..ca8ffedc72 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.scss +++ b/ui-ngx/src/app/shared/components/color-input.component.scss @@ -25,5 +25,8 @@ .tb-color-preview { margin-left: 8px; margin-right: 5px; + &.no-margin { + margin: 0; + } } } diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index 86199ea627..08432d6967 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -22,6 +22,7 @@ import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_AC import { TranslateService } from '@ngx-translate/core'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DialogService } from '@core/services/dialog.service'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-color-input', @@ -37,6 +38,10 @@ import { DialogService } from '@core/services/dialog.service'; }) export class ColorInputComponent extends PageComponent implements OnInit, ControlValueAccessor { + @Input() + @coerceBoolean() + asBoxInput = false; + @Input() icon: string; @@ -95,7 +100,8 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro constructor(protected store: Store, private dialogs: DialogService, private translate: TranslateService, - private fb: UntypedFormBuilder) { + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { super(store); } @@ -154,6 +160,7 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro this.colorFormGroup.patchValue( {color}, {emitEvent: true} ); + this.cd.markForCheck(); } } ); @@ -161,5 +168,6 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro clear() { this.colorFormGroup.get('color').patchValue(null, {emitEvent: true}); + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index a8cd40f9b8..af0373b7a9 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -38,7 +38,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; -import { isEqual } from '@core/utils'; +import { isDefinedAndNotNull, isEqual } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ @@ -291,7 +291,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit async writeValue(value: string | EntityId | null): Promise { this.searchText = ''; - if (value !== null && (typeof value === 'string' || (value.entityType && value.id))) { + if (isDefinedAndNotNull(value) && (typeof value === 'string' || (value.entityType && value.id))) { let targetEntityType: EntityType; let id: string; if (typeof value === 'string') { diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index 925e8beeac..5cf7cb6de7 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -15,8 +15,8 @@ limitations under the License. --> -
- {{materialIconFormGroup.get('icon').value}} +
+ {{materialIconFormGroup.get('icon').value}} {{ label }} @@ -28,3 +28,8 @@
+ + {{materialIconFormGroup.get('icon').value}} + diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.scss b/ui-ngx/src/app/shared/components/material-icon-select.component.scss index d17df37a47..6bfd308ae5 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.scss +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.scss @@ -14,11 +14,28 @@ * limitations under the License. */ :host { - .mat-icon.icon-value { - padding: 4px; - margin: 12px 4px 4px; - cursor: pointer; - border: solid 1px rgba(0, 0, 0, .27); - box-sizing: initial; + .mat-icon { + &.icon-value { + padding: 4px; + margin: 12px 4px 4px; + cursor: pointer; + border: solid 1px rgba(0, 0, 0, .27); + box-sizing: initial; + } + &.icon-box { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + cursor: pointer; + box-sizing: border-box; + padding: 8px; + height: 40px; + width: 40px; + font-size: 22px; + vertical-align: middle; + &.disabled { + cursor: initial; + color: rgba(0, 0, 0, 0.38); + } + } } } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 715c9161a3..b8bb7ed25b 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -22,6 +22,7 @@ import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_AC import { DialogService } from '@core/services/dialog.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { TranslateService } from '@ngx-translate/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-material-icon-select', @@ -37,9 +38,16 @@ import { TranslateService } from '@ngx-translate/core'; }) export class MaterialIconSelectComponent extends PageComponent implements OnInit, ControlValueAccessor { + @Input() + @coerceBoolean() + asBoxInput = false; + @Input() label = this.translate.instant('icon.icon'); + @Input() + color: string; + @Input() disabled: boolean; @@ -73,7 +81,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit constructor(protected store: Store, private dialogs: DialogService, private translate: TranslateService, - private fb: UntypedFormBuilder) { + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { super(store); } @@ -126,6 +135,7 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.patchValue( {icon}, {emitEvent: true} ); + this.cd.markForCheck(); } } ); @@ -134,5 +144,6 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit clear() { this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/app/shared/components/public-api.ts b/ui-ngx/src/app/shared/components/public-api.ts index 2443bc6d43..1dd31182f1 100644 --- a/ui-ngx/src/app/shared/components/public-api.ts +++ b/ui-ngx/src/app/shared/components/public-api.ts @@ -22,3 +22,4 @@ export * from './js-func.component'; export * from './script-lang.component'; export * from './slack-conversation-autocomplete.component'; export * from './notification/template-autocomplete.component'; +export * from './toggle-header.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html similarity index 70% rename from ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html rename to ui-ngx/src/app/shared/components/toggle-header.component.html index ed39fca25d..e36cdbd592 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -15,9 +15,12 @@ limitations under the License. --> - - {{ option.name }} + {{ option.name }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss b/ui-ngx/src/app/shared/components/toggle-header.component.scss similarity index 81% rename from ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss rename to ui-ngx/src/app/shared/components/toggle-header.component.scss index 1e901b056a..a9542012d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss +++ b/ui-ngx/src/app/shared/components/toggle-header.component.scss @@ -14,7 +14,8 @@ * limitations under the License. */ -@import "../../../../../../../scss/constants"; +@import "../../../theme"; +@import "../../../scss/constants"; :host ::ng-deep { .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { @@ -47,8 +48,8 @@ &.mat-button-toggle-checked { .mat-button-toggle-button { background: #F3F6FA; - color: #305680; - border: 1px solid #305680; + color: $tb-primary-color; + border: 1px solid $tb-primary-color; border-radius: 20px; .mat-button-toggle-label-content { @@ -58,9 +59,30 @@ } } } + &.tb-fill { + .mat-button-toggle.mat-button-toggle-appearance-standard { + &.mat-button-toggle-checked { + .mat-button-toggle-button { + background: $tb-primary-color; + color: #FFFFFF; + } + } + } + &.tb-invert { + .mat-button-toggle.mat-button-toggle-appearance-standard { + color: rgba(255, 255, 255, 0.8); + &.mat-button-toggle-checked { + .mat-button-toggle-button { + background: #FFFFFF; + color: $tb-primary-color; + } + } + } + } + } } @media #{$mat-md-lg} { - .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header:not(.tb-ignore-md-lg) { height: 24px; .mat-button-toggle.mat-button-toggle-appearance-standard { .mat-button-toggle-button { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts similarity index 88% rename from ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts rename to ui-ngx/src/app/shared/components/toggle-header.component.ts index b8fd5c63bb..a7f37f53a4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -38,12 +38,15 @@ import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button- import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { BreadCrumb } from '@shared/components/breadcrumb'; export interface ToggleHeaderOption { name: string; value: any; } +export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; + @Component({ selector: 'tb-toggle-header', templateUrl: './toggle-header.component.html', @@ -67,6 +70,13 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit { @coerceBoolean() useSelectOnMdLg = true; + @Input() + @coerceBoolean() + ignoreMdLgSize = false; + + @Input() + appearance: ToggleHeaderAppearance = 'stroked'; + isMdLg: boolean; private observeBreakpointSubscription: Subscription; @@ -87,4 +97,8 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit { } ); } + + trackByHeaderOption(index: number, option: ToggleHeaderOption){ + return option.value; + } } diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 4ee89fc89f..7c978fb4c9 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -749,6 +749,14 @@ export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPag export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1); +export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter => ({ + type: AliasFilterType.singleEntity, + singleEntity: { + entityType: EntityType.DEVICE, + id: deviceId + } +}); + export interface EntityCountQuery { entityFilter: EntityFilter; keyFilters?: Array; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index b20565a50b..bfe9021926 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -162,6 +162,8 @@ export interface WidgetTypeDescriptor { settingsDirective?: string; dataKeySettingsDirective?: string; latestDataKeySettingsDirective?: string; + hasBasicMode?: boolean; + basicModeDirective?: string; defaultConfig: string; sizeX: number; sizeY: number; @@ -252,18 +254,16 @@ export interface LegendConfig { showLatest: boolean; } -export function defaultLegendConfig(wType: widgetType): LegendConfig { - return { - direction: LegendDirection.column, - position: LegendPosition.bottom, - sortDataKeys: false, - showMin: false, - showMax: false, - showAvg: wType === widgetType.timeseries, - showTotal: false, - showLatest: false - }; -} +export const defaultLegendConfig = (wType: widgetType): LegendConfig => ({ + direction: LegendDirection.column, + position: LegendPosition.bottom, + sortDataKeys: false, + showMin: false, + showMax: false, + showAvg: wType === widgetType.timeseries, + showTotal: false, + showLatest: false +}); export enum ComparisonResultType { PREVIOUS_VALUE = 'PREVIOUS_VALUE', @@ -320,6 +320,7 @@ export interface DataKey extends KeyInfo { export enum DatasourceType { function = 'function', + device = 'device', entity = 'entity', entityCount = 'entityCount', alarmCount = 'alarmCount' @@ -328,6 +329,7 @@ export enum DatasourceType { export const datasourceTypeTranslationMap = new Map( [ [ DatasourceType.function, 'function.function' ], + [ DatasourceType.device, 'device.device' ], [ DatasourceType.entity, 'entity.entity' ], [ DatasourceType.entityCount, 'entity.entities-count' ], [ DatasourceType.alarmCount, 'entity.alarms-count' ] @@ -343,6 +345,7 @@ export interface Datasource { entityType?: EntityType; entityId?: string; entityName?: string; + deviceId?: string; entityAliasId?: string; filterId?: string; unresolvedStateEntity?: boolean; @@ -363,7 +366,7 @@ export interface Datasource { [key: string]: any; } -export function datasourcesHasAggregation(datasources?: Array): boolean { +export const datasourcesHasAggregation = (datasources?: Array): boolean => { if (datasources) { const foundDatasource = datasources.find(datasource => { const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries && @@ -375,9 +378,9 @@ export function datasourcesHasAggregation(datasources?: Array): bool } } return false; -} +}; -export function datasourcesHasOnlyComparisonAggregation(datasources?: Array): boolean { +export const datasourcesHasOnlyComparisonAggregation = (datasources?: Array): boolean => { if (!datasourcesHasAggregation(datasources)) { return false; } @@ -392,7 +395,7 @@ export function datasourcesHasOnlyComparisonAggregation(datasources?: Array { if (settings) { const keys = Object.keys(settings); for (const key of keys) { @@ -711,7 +718,7 @@ function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings { } } return settings; -} +}; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 60877bd20d..1d755c3bb1 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -190,6 +190,7 @@ import { } from '@shared/layout/layout.directives'; import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; +import { ToggleHeaderComponent } from '@shared/components/toggle-header.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -358,7 +359,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) GtMdLgLayoutAlignDirective, GtMdLgLayoutGapDirective, GtMdLgShowHideDirective, - ColorPickerComponent + ColorPickerComponent, + ToggleHeaderComponent ], imports: [ CommonModule, @@ -583,7 +585,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) GtMdLgLayoutAlignDirective, GtMdLgLayoutGapDirective, GtMdLgShowHideDirective, - ColorPickerComponent + ColorPickerComponent, + ToggleHeaderComponent ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 2fab5b1e85..6a1ee20447 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -35,6 +35,7 @@ "edit-mode": "Edit mode", "enter-edit-mode": "Enter edit mode", "decline-changes": "Decline changes", + "decline": "Decline", "close": "Close", "back": "Back", "run": "Run", @@ -1114,6 +1115,10 @@ "function-types": "Function types", "function-type": "Function type", "function-types-required": "Function types are required.", + "data-keys": "Data keys", + "data-key": "Data key", + "data-keys-required": "Data keys are required.", + "data-key-required": "Data key is required.", "alarm-keys": "Alarm data keys", "alarm-key": "Alarm data key", "alarm-key-functions": "Alarm key functions", @@ -1974,6 +1979,7 @@ "no-aliases-found": "No aliases found.", "no-alias-matching": "'{{alias}}' not found.", "create-new-alias": "Create a new one!", + "create-new": "Create new", "key": "Key", "key-name": "Key name", "no-keys-found": "No keys found.", @@ -2434,6 +2440,7 @@ "add-filter-prompt": "Please add filter", "no-filter-matching": "'{{filter}}' not found.", "create-new-filter": "Create a new one!", + "create-new": "Create new", "filter-required": "Filter is required.", "operation": { "operation": "Operation", @@ -3992,6 +3999,10 @@ "settings-form-selector": "Settings form selector", "data-key-settings-form-selector": "Data key settings form selector", "latest-data-key-settings-form-selector": "Latest data key settings form selector", + "has-basic-mode": "Has basic mode", + "basic-mode-form-selector": "Basic mode form selector", + "basic-mode": "Basic", + "advanced-mode": "Advanced", "javascript": "Javascript", "js": "JS", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", @@ -4106,6 +4117,9 @@ "data": "Data", "settings": "Settings", "advanced": "Advanced", + "appearance": "Appearance", + "widget-card": "Widget card", + "mobile": "Mobile", "title": "Title", "title-tooltip": "Title Tooltip", "general-settings": "General settings", @@ -4126,12 +4140,16 @@ "desktop-hide": "Hide widget in desktop mode", "units": "Special symbol to show next to value", "decimals": "Number of digits after floating point", + "units-short": "Units", + "decimals-short": "Decimals", "timewindow": "Timewindow", "use-dashboard-timewindow": "Use dashboard timewindow", + "use-widget-timewindow": "Use widget timewindow", "display-timewindow": "Display timewindow", "legend": "Legend", "display-legend": "Display legend", "datasources": "Datasources", + "datasource": "Datasource", "maximum-datasources": "Maximum { count, plural, =1 {1 datasource is allowed.} other {# datasources are allowed} }", "timeseries-key-error": "At least one timeseries data key should be specified", "datasource-type": "Type", @@ -4164,9 +4182,20 @@ "icon-size": "Icon size", "advanced-settings": "Advanced settings", "data-settings": "Data settings", + "limits": "Limits", "no-data-display-message": "\"No data to display\" alternative message", "data-page-size": "Maximum entities per datasource", - "settings-component-not-found": "Settings form component not found for selector '{{selector}}'" + "settings-component-not-found": "Settings form component not found for selector '{{selector}}'", + "preview": "Preview", + "set": "Set", + "set-message": "Set message", + "card-title": "Card title", + "advanced-title-style": "Advanced title style", + "card-style": "Card style", + "text": "Text", + "background": "Background", + "advanced-widget-style": "Advanced widget style", + "card-buttons": "Card buttons" }, "widget-type": { "import": "Import widget type", @@ -5092,6 +5121,7 @@ "markdown-css": "Markdown/HTML CSS" }, "simple-card": { + "label": "Label", "label-position": "Label position", "label-position-left": "Left", "label-position-top": "Top" diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 296e9b26fe..3d4029d564 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1087,6 +1087,27 @@ mat-label { border-radius: 50%; box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); + &.box { + border: none; + box-shadow: none; + border-radius: 4px; + .tb-color-result { + border: 1px solid rgba(0, 0, 0, 0.12); + } + &.disabled { + cursor: initial; + .tb-color-result { + background: rgba(0, 0, 0, 0.38); + } + } + } + + &.small { + width: 18px; + min-width: 18px; + height: 18px; + } + @include tb-checkered-bg(); .tb-color-result {