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 5119d01671..0829c612d0 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -16,8 +16,10 @@ "charts.bars", "charts.pie", "charts.pie_chart_js", - "charts.doughnut_chart_js", + "doughnut", + "horizontal_doughnut", "charts.polar_area_chart_js", - "charts.radar_chart_js" + "charts.radar_chart_js", + "charts.doughnut_chart_js" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/doughnut.json b/application/src/main/data/json/system/widget_types/doughnut.json index 6056cb5a44..f5a890ad3a 100644 --- a/application/src/main/data/json/system/widget_types/doughnut.json +++ b/application/src/main/data/json/system/widget_types/doughnut.json @@ -1,30 +1,29 @@ { - "fqn": "charts.doughnut_chart_js", + "fqn": "doughnut", "name": "Doughnut", "deprecated": false, - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAR7UlEQVR42u2deXhU1RXA+a/a3a+t1mr/qGtxLVYtiBUtqK2y77IpCLIFimUTlU9FQOGDAhVZJGEJiyBgVkhCzE5WkpB9IyEhZCf77G/mvdvzmDgzSSZhmLzz3n1v7vnOH36I5s28X+49+xlEmDBBkEHsK2DCwJJDytrKd2Xv3pj2xdrEDxf/sGzW+Xemh89y1YmhU+dEzFvyw/I1ies2pG76b+auI4VHL1RF598ouGFsFojAvkPfBYvjSX4Tf6KQ+yjePPa0wT+Hc/yrghuFo4PGe60TQ6csi12xK/uriMrIivYKK29jYGlcbAK53GDbdcky6XvjI/t0f9rj1A/izI6/1mBoHAhYvTibujrhg2PFJ0paSnmBZ2BpRzrMwulizi/KNCRA7wqTq84IMTr+vslqkhAsV33r3Oytl7bHVsfpOT0DS61isZGYKuvKGNNj3+j64smhwwOdb1oQBCSwHDohZMrm9C2pdWlW3srAUo2A8bQ2zvyUv/6WPDn0wb06s4stNDdyPjZbP55hs77O2VvRfpWBRa/wgnhEzQo1es6Tq5a3OQ2grRnb5QHLoR8mrU+rT4fDkoFFkRitJCCXe+mYwTuk7Bpb5byVgstDZAbLrgsuLA6rOGexWRhYCouVJxAyGHpEPxCk7HoozxlxyGrIVgQsu848P+d02VmO5xhYylx858qtLx83DBwpu36W5Iw4XO+sURAsu86LWhBRGaXeCIUqwUqvtb12UjKk7Do33Blx0HF6xcGyq1/MisLmIgYWurSbhQ0XzeDESUsV6KgTBpfjkKcELNAxQRMga9Ru7mBgoQj4S6eKuGcO6iVHyq6P7tfxLj7Z1LAZ9LAFCjlKuBlVlIhUB1gNemGmt3EEz7Wm0/naPk3dQBVYdl2f/GmLqZWBJY1EXbXiHVSumlLjDJIeL/6WQrBAZ5ybk16fwcAakEA0HCyqB/CRsuvJIqeHn1ybQidYdgWry2wzM7C8kWsdvOSuX/+6JdUZmbzaXkkzWHaHscHQwMC6PblUZ3v2kF5OqkCXRpkcD9BmbqccLPu1mN9cwMDyVOBK6lEvJY+O/s7gEtO30g8W6LjgSeEV5xlYt46ng1ElP1J2fdK/W5nU2JCJqmAL9EBeAG0JbIrAggrPVTEmpaiya6vJ+XpWxq9RC1igWy9to6oMmhawoAh9UaTCVIFC7bLjkfzzDqoILNBNaV/SUzlIBVhQ9zInzKg4VaAhZc4XE3MtTl1ggX6S8hklVTeDaKBqahAVVIFCq4XjwYpbSlQHFui6pI9pYEthsKCgat45EyVUgUKNvOPZoElQjWCBfpa60SbYfBcssJNXx5rpoQp08vdGl7i/WaVggW7P3KFsxlpJsDYl00UV6POHXdp1iKBesEAPFwb6IlgBORxtVIFCXtLAOX/RF0UvVTVboRXhvgVW8nXbQ3t1FIIFWtzsrAaGIQ6qBmts8MTLjTm+AlatTvir7HlAzzXyqjPiANkSVYN1szXjbfBCtA8WVMKMOW2glqrHD+iDSp1gGThDja623dzuCDyCJ99p6YQ/hGAEtDKfLQv6X/bXMJdmcth0atmCFIL8PT9yg0WbGwgKPdNLokzHC7miZt7mbVMMlMlXdlTChJkv0rdOC59BG1u7L+/RMljh5VZ6eIKq1PWJ5rRam03qDiuIIeXdyN+Tsx+mgNDD1sXaZG2C1WQQ5KkwvqXf93aYMbrSasXv2IMLFK5LKFSHThsKjK05cKdrEKz55xWOsEON17p4c1mrAi2g1zqvgSk2PmSy4llqrYH1XbGSUauH9unAtrveqXBXcaOhcUfWLggBKMhWbHW8dsBqNAhP+it2CcJQtSutFDWqw+kFs2WUAmtpzHJ5SgLlAGulQuV7kJ8JLaNxshkki+DkgLG5ciIFFzEkeWBeoUZOrOwG2wNKULUowuRaDkqhdFg6wOiRq05rA8TetOMVQg37uDNyh0NhNuSpItXMAIqsvDA5FDG4Ov/CQhjpprXIO/TbyEzVsEB9XpPKRv9AZHVe1HuSIwUDm08Un7TYev2OCYItKc4aHaFWsCB7I8lINM91SpCxxajKaYsQYYJ1BBJSBQmAJmOTmzukstw4a4J+5POGmeOIlVMlWEfyZT2uIOxpUvMMYoimwijlgSO1MHpJZkO2mx9gMpq3bgCkHMqFnlEfWNB18+JR+Y4rqG82q38FBOSCoPLTa6QgRwnTU900gfE8F3pW/+pQV6rEQ2vKG8RsUhlYxwrkO66WXTBZtbLzAYJM2zN3ejGcDUKvraY2N3dfSaFx6hs9kHIeWkGn1ASWVcbjCsIKNm2tRYJz6/PUzZ5TtSJuJdTwuGG0s8P00X/6Qqrr0Jo5Hs4z1YAFYUnZpi24VhJrRsCV88SWh6Eg4pi/3pF0m407Gdg/Ug61XYxXDVjTgo3yBNbrdJrd4QZ+4tzIBf3UHENZTqdF5+bAy88xTHzNQ6pATe8vVAdYkJiTIdT+5/3d2uE1KbAKZVLotN5UvR+/qrS1zM3d19JsXPGe50g5lC8rUQFYMDBd5ul7GhaIy7siBQs446oT3DQMWjnLob1eIGVXy1fbaAcLIkl/CUA322eHGn1njennaZvtKeSDBYeNVqObuy8zXT/6Za+pEk348aMIZ6EarGB8s/3pAH29zofW48KEdxhRVKOrcXP31dUaF84aCFIOtSbGUg3Wwgj0CpmzJVbCxGy27NoiCVJdJvzHK+kFCzx/T7ZODkRh2pGvIwUp5IQY/esvSEiVqK8PF/Q6SsE6j9yEA85mfhPv01DV1RjnTZMYKcdtGHeBUrCWX8C9B/2iTL5MFRd2Fgkpu5o3racRLMg6P3FAj9oQUdHm28dVfa30N6CrbzhuJLFaqQMrs96GelytiTUTnxfzto2oh5YtJ4s6sHZnWlDBKm3hGVh81VX9qL/hgcUF+lMH1mzM7Vyug/Z8XExr/PDAMq32owssqJN5HNPA+r6Uxa5+jLMnxSHehm+OkMrMkgasLEwDa0iA3sS4cv4Sc4YJr+KxxRcVUATWwVzEetFPEpnZ3k0sO7cgmlkS1ZRKA9baOMSKhoRqG4Op222YlY4Yzdq+mSKwJpzFstwHf6Nj92Cv29BqGD8SCSyj31xawIJKAzzLfW448wfdBbQ2rMMKk44eQQSeCrCqO3i8e/BwHscw6i1c8GlE+732OhVgxVYh5p7LWFzUbaS04gpi/D0rnQqw8FoIH92vszGu3NsfPNxZWGUOEWFUgLUjAyuZ88YpA0OoLwErGyvicOQAFWDBYE8ksFZEmxhAfdrv2zdjRRy2baQCLLy9cHuzLQygPu33b49gZQzXraACLLxNE5EVLITVdzArPhorlLV8PhVgjTiGBVZqLYu59x1/L8jFAuvd6VSAhTddrfAG8wn7jjhcv4YVI50+hgqw8PZNKD6ZneqAQ3sbFlhjXqECLLx8TodZYACpVCQA6+F9WGF3Fh31abDwZstocvYVA8tTgYlCSGA1GxlYPgzWkABmvCsSIW0iCT9B0eR7qADrhUA9K21QQAzFWGClP0wFWKNOYAVIsxtYgLRvaU/CAivzOSrAwkvphJSxlE7fcuMsFlg5I6kAC1ZCIIG1M4MlofuW6i1YYBVMogIsvBadf7OymX6kdAEWWCV0JKF3XsIq9INLlvHTp1wegQVW1edUgHUKbXcczEXiWSTLrQgcSboLC6yGY1SAlViN2F/v4yP8+hRdFhZVoO3JVIB1tQ2x/Wv/ZWa/u5PafYhgWeqoAAt2JOHNtH2HNay6lcKpWFQl/ZoQgQqwQMafwYo4ALIcuwx7GlgWcvG3WGBlD5fkGaUBC69RBzSlhsXfu0tbHOI9WOZHEViBmFt62fTRngLvHg+s+kMUgXWpDtExhKADK8zqdg+m3IsIlj6PIrAsNjIYcycFW3PiFLwUod1yF2gaFQkyIwRxuC38zxlRXZL3L0SwckZJ9ZjqGMf94F5dWStzDgnRF5CEOxDBuvYldWBlIS8QWM4S0iCQHsajCrQzkzqwrMgrT9ihRUxVJPEXiFSl/AHaYKkDC2RxpIkdWohS/DbucVU8R8KHlRKssCu4a+XEQ8tnq+D1+SThTlywGk9QChZEmwYjL8KcGuSbHWECyX0dl6qkXxFrO6VggSyKRF/de7zQ98bdNhzFpQq0aJa0jywxWKH4y8af8tc36H3p2OKaScp96GA1h1INFtyG8OKx2YKV5j50CUJrAzZVyfeKmSKawQKB1TfYYIHCqGaf4Aq1oM+hV5ZJ/uDSgwWOmwxgPbJPB5lvjVMF9ceQvEMH6w5iKFQBWCDTgo0ysPXcIX29TrvGlqWBpD0gx3EFyUcEQQEr9IpVBrBAYTmURZPHFm8il1+SgyrRbA9XDVhQTDw8UC8PW/PPm7RWuwytXfkTZKIqY7CEaRx0sAjmHpTeukBLbAk2UjRbJqpAa/cgfQ4ssOBNv3hULxtbsH1OC3cinFXYCUFXTXuI8GaVgUWQC+F76xG1L6CDd4zX1OW+vP0g3qdBBAuOkGFyWVpjTxvU3YwP4/lyXpGVqozHxANSjWARzLEOrvrMQZ1R1TXxuhxxiJ6cVEldyyA3WHCK4PWydtXS7NHV9YhmXd9B6g6ohqo6f1mioN01a5joJagXLJC8Jh7qqPDAyugRf28O6apbKpgspm8pv/4KJsqNlKh3ks4M7A83SIYvcFUMVi3NkfzuVoI+l1z8jfMbTP0jaTguySQCyd0/8SZKuV8JqqDXeYkMn1AOsJoMwpMIJQ+re3RIWxpF/7n395j7KkYuzHuBThsY8qkIUvbCdq5FI2CBfFfM4bqB/edAEn9KSuYR01WFkYIHKHlXfBilqBJDDIfl+ayDZPtWJSwuHdLbDSyZe+vvNPHnpHSheGDIL4YiUrpIfAAFkQKFTJFchoF8YLUYBahHQHEDq7feXpUI5PPBxhfwR7rBj7gRTHL/idtl6ukleL/oLsglg+T8vY2osCK6gV6UTYIZCyOBJI8TwviDtnhStlj8EYrz5Ph1ajkn57seJPOdADOJBkLV4bx+3UDvFIaYQS6ldr8YqPR6JAb8hzCnBQo+C6aQ5Lup4QmxRpQusMw2Mu6Ml5ssVsaYPHIDB9QFdZc45rr0PfF6hbkuHSnEUCLeINZWYtPf9BKMxFIv/mFHGmk6JU7xh2Pv8t8VCHLeVjiUN2ocLDHUrBOevX1jq5cbaJavFE7VCsE883X537ICYIFk1ttuay8ruIGGHoYQ+HcMGk8c4fZERV6xMmCBHMzlBuAGbmHQeKTK5UwVAwvkixSLV25gKPoUA21oxVoFX66SYAkeLHg6kMP1jDRe/B2DxoNFS/OUTZIqCRa5uXygn+FHbtxA+euW1KiF07GrYmgHyx6AeMtdH+LoHm4gRLFhQiaD5pYKgX5e+QHmyoMFAom/Hts0b2YDhdvOBjLNe5PwVOziowIscrOrZ0mUyeEG1gwoG+irCuNDeFrGpdAClt3esid8mBvojcLCVYGiDjiKwCI3a+R7UiVJNlDzWr6KtkJZusDqdUE2k/Q/M276ja3/jNTspvDVUQwWcwM9qTOG+hwqhWKwzLUkayijp++ahaHEXE3t26P7KoR4TPlKKsov6dI7xPoqnur9QnSDZZfWaLH2g/HUdf3dR5rD6H9pagBLvBZrSN5oRpXYDSFj3boPgGUXBZs8abDTxc4t1Uw+URVYYgCi5eYGLJ+yuu4Uq59l6TL1YbDs0pYg1nH7hOs3TIY5CwysbmEusZcBRmhqFSn4aA3HkAaEMrBuSRdHavdqzWeE1qO6ANSpaAwsD8NdRrG4O+MJ1SOV/og4bZaCaioGVje+xDqInH+oMuApdv2HUlWewMDqJbpscuV90T+nH6nke8iV5cRQrL2XoEWwuswvG2mNIUUzcPcoe711Emb5wVA4Oqo9GVheibWNNJ0Wu1Zk2Pp3yzEkAHrjya5ufU2LD4DlaoR1pJOqDeIaXBgEItPh9Evxx0GHbWememMHDKzbuShhOAz4klDRmz1cyvkwcM1lv0jK/MSQAeyFk2EKFwOLaoE1bhDQh+HYlZ+SsqXiYCNwMC89LV6gyb//Ue8WBz3CP6Q/SjKfE/9C/jgRzaqNYjCzPYmYrnk/CImBxYQJA4uJMvJ/KZzDcnj6uUcAAAAASUVORK5CYII=", - "description": "Displays the latest values of the attributes or time-series data for multiple entities in a doughnut chart. Supports numeric values only.", + "image": "data:image/png;base64,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", + "description": "Displays the latest values of the attributes or time-series data in a doughnut chart. Supports numeric values only.", "descriptor": { "type": "latest", - "sizeX": 7, - "sizeY": 5, - "resources": [ - { - "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" - } - ], - "templateHtml": "\n", + "sizeX": 4, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.doughnutWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.doughnutWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '380px',\n previewHeight: '300px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind power', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar power', type: 'timeseries', color: '#FF4D5A' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", "settingsSchema": "", - "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-doughnut-chart-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "dataKeySettingsSchema": "", + "settingsDirective": "tb-doughnut-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-doughnut-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"totalValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1\"},\"totalValueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showLegend\":true,\"legendPosition\":\"bottom\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.38)\",\"legendValueFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"legendValueColor\":\"rgba(0, 0, 0, 0.87)\",\"showTooltip\":true,\"tooltipValueType\":\"percentage\",\"tooltipValueDecimals\":0,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" }, "externalId": null, "tags": [ "ring", "circle", - "pie chart" + "pie chart", + "donut" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/doughnut_deprecated.json b/application/src/main/data/json/system/widget_types/doughnut_deprecated.json new file mode 100644 index 0000000000..f0fc4e30c0 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/doughnut_deprecated.json @@ -0,0 +1,30 @@ +{ + "fqn": "charts.doughnut_chart_js", + "name": "Doughnut", + "deprecated": true, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAR7UlEQVR42u2deXhU1RXA+a/a3a+t1mr/qGtxLVYtiBUtqK2y77IpCLIFimUTlU9FQOGDAhVZJGEJiyBgVkhCzE5WkpB9IyEhZCf77G/mvdvzmDgzSSZhmLzz3n1v7vnOH36I5s28X+49+xlEmDBBkEHsK2DCwJJDytrKd2Xv3pj2xdrEDxf/sGzW+Xemh89y1YmhU+dEzFvyw/I1ies2pG76b+auI4VHL1RF598ouGFsFojAvkPfBYvjSX4Tf6KQ+yjePPa0wT+Hc/yrghuFo4PGe60TQ6csi12xK/uriMrIivYKK29jYGlcbAK53GDbdcky6XvjI/t0f9rj1A/izI6/1mBoHAhYvTibujrhg2PFJ0paSnmBZ2BpRzrMwulizi/KNCRA7wqTq84IMTr+vslqkhAsV33r3Oytl7bHVsfpOT0DS61isZGYKuvKGNNj3+j64smhwwOdb1oQBCSwHDohZMrm9C2pdWlW3srAUo2A8bQ2zvyUv/6WPDn0wb06s4stNDdyPjZbP55hs77O2VvRfpWBRa/wgnhEzQo1es6Tq5a3OQ2grRnb5QHLoR8mrU+rT4fDkoFFkRitJCCXe+mYwTuk7Bpb5byVgstDZAbLrgsuLA6rOGexWRhYCouVJxAyGHpEPxCk7HoozxlxyGrIVgQsu848P+d02VmO5xhYylx858qtLx83DBwpu36W5Iw4XO+sURAsu86LWhBRGaXeCIUqwUqvtb12UjKk7Do33Blx0HF6xcGyq1/MisLmIgYWurSbhQ0XzeDESUsV6KgTBpfjkKcELNAxQRMga9Ru7mBgoQj4S6eKuGcO6iVHyq6P7tfxLj7Z1LAZ9LAFCjlKuBlVlIhUB1gNemGmt3EEz7Wm0/naPk3dQBVYdl2f/GmLqZWBJY1EXbXiHVSumlLjDJIeL/6WQrBAZ5ybk16fwcAakEA0HCyqB/CRsuvJIqeHn1ybQidYdgWry2wzM7C8kWsdvOSuX/+6JdUZmbzaXkkzWHaHscHQwMC6PblUZ3v2kF5OqkCXRpkcD9BmbqccLPu1mN9cwMDyVOBK6lEvJY+O/s7gEtO30g8W6LjgSeEV5xlYt46ng1ElP1J2fdK/W5nU2JCJqmAL9EBeAG0JbIrAggrPVTEmpaiya6vJ+XpWxq9RC1igWy9to6oMmhawoAh9UaTCVIFC7bLjkfzzDqoILNBNaV/SUzlIBVhQ9zInzKg4VaAhZc4XE3MtTl1ggX6S8hklVTeDaKBqahAVVIFCq4XjwYpbSlQHFui6pI9pYEthsKCgat45EyVUgUKNvOPZoElQjWCBfpa60SbYfBcssJNXx5rpoQp08vdGl7i/WaVggW7P3KFsxlpJsDYl00UV6POHXdp1iKBesEAPFwb6IlgBORxtVIFCXtLAOX/RF0UvVTVboRXhvgVW8nXbQ3t1FIIFWtzsrAaGIQ6qBmts8MTLjTm+AlatTvir7HlAzzXyqjPiANkSVYN1szXjbfBCtA8WVMKMOW2glqrHD+iDSp1gGThDja623dzuCDyCJ99p6YQ/hGAEtDKfLQv6X/bXMJdmcth0atmCFIL8PT9yg0WbGwgKPdNLokzHC7miZt7mbVMMlMlXdlTChJkv0rdOC59BG1u7L+/RMljh5VZ6eIKq1PWJ5rRam03qDiuIIeXdyN+Tsx+mgNDD1sXaZG2C1WQQ5KkwvqXf93aYMbrSasXv2IMLFK5LKFSHThsKjK05cKdrEKz55xWOsEON17p4c1mrAi2g1zqvgSk2PmSy4llqrYH1XbGSUauH9unAtrveqXBXcaOhcUfWLggBKMhWbHW8dsBqNAhP+it2CcJQtSutFDWqw+kFs2WUAmtpzHJ5SgLlAGulQuV7kJ8JLaNxshkki+DkgLG5ciIFFzEkeWBeoUZOrOwG2wNKULUowuRaDkqhdFg6wOiRq05rA8TetOMVQg37uDNyh0NhNuSpItXMAIqsvDA5FDG4Ov/CQhjpprXIO/TbyEzVsEB9XpPKRv9AZHVe1HuSIwUDm08Un7TYev2OCYItKc4aHaFWsCB7I8lINM91SpCxxajKaYsQYYJ1BBJSBQmAJmOTmzukstw4a4J+5POGmeOIlVMlWEfyZT2uIOxpUvMMYoimwijlgSO1MHpJZkO2mx9gMpq3bgCkHMqFnlEfWNB18+JR+Y4rqG82q38FBOSCoPLTa6QgRwnTU900gfE8F3pW/+pQV6rEQ2vKG8RsUhlYxwrkO66WXTBZtbLzAYJM2zN3ejGcDUKvraY2N3dfSaFx6hs9kHIeWkGn1ASWVcbjCsIKNm2tRYJz6/PUzZ5TtSJuJdTwuGG0s8P00X/6Qqrr0Jo5Hs4z1YAFYUnZpi24VhJrRsCV88SWh6Eg4pi/3pF0m407Gdg/Ug61XYxXDVjTgo3yBNbrdJrd4QZ+4tzIBf3UHENZTqdF5+bAy88xTHzNQ6pATe8vVAdYkJiTIdT+5/3d2uE1KbAKZVLotN5UvR+/qrS1zM3d19JsXPGe50g5lC8rUQFYMDBd5ul7GhaIy7siBQs446oT3DQMWjnLob1eIGVXy1fbaAcLIkl/CUA322eHGn1njennaZvtKeSDBYeNVqObuy8zXT/6Za+pEk348aMIZ6EarGB8s/3pAH29zofW48KEdxhRVKOrcXP31dUaF84aCFIOtSbGUg3Wwgj0CpmzJVbCxGy27NoiCVJdJvzHK+kFCzx/T7ZODkRh2pGvIwUp5IQY/esvSEiVqK8PF/Q6SsE6j9yEA85mfhPv01DV1RjnTZMYKcdtGHeBUrCWX8C9B/2iTL5MFRd2Fgkpu5o3racRLMg6P3FAj9oQUdHm28dVfa30N6CrbzhuJLFaqQMrs96GelytiTUTnxfzto2oh5YtJ4s6sHZnWlDBKm3hGVh81VX9qL/hgcUF+lMH1mzM7Vyug/Z8XExr/PDAMq32owssqJN5HNPA+r6Uxa5+jLMnxSHehm+OkMrMkgasLEwDa0iA3sS4cv4Sc4YJr+KxxRcVUATWwVzEetFPEpnZ3k0sO7cgmlkS1ZRKA9baOMSKhoRqG4Op222YlY4Yzdq+mSKwJpzFstwHf6Nj92Cv29BqGD8SCSyj31xawIJKAzzLfW448wfdBbQ2rMMKk44eQQSeCrCqO3i8e/BwHscw6i1c8GlE+732OhVgxVYh5p7LWFzUbaS04gpi/D0rnQqw8FoIH92vszGu3NsfPNxZWGUOEWFUgLUjAyuZ88YpA0OoLwErGyvicOQAFWDBYE8ksFZEmxhAfdrv2zdjRRy2baQCLLy9cHuzLQygPu33b49gZQzXraACLLxNE5EVLITVdzArPhorlLV8PhVgjTiGBVZqLYu59x1/L8jFAuvd6VSAhTddrfAG8wn7jjhcv4YVI50+hgqw8PZNKD6ZneqAQ3sbFlhjXqECLLx8TodZYACpVCQA6+F9WGF3Fh31abDwZstocvYVA8tTgYlCSGA1GxlYPgzWkABmvCsSIW0iCT9B0eR7qADrhUA9K21QQAzFWGClP0wFWKNOYAVIsxtYgLRvaU/CAivzOSrAwkvphJSxlE7fcuMsFlg5I6kAC1ZCIIG1M4MlofuW6i1YYBVMogIsvBadf7OymX6kdAEWWCV0JKF3XsIq9INLlvHTp1wegQVW1edUgHUKbXcczEXiWSTLrQgcSboLC6yGY1SAlViN2F/v4yP8+hRdFhZVoO3JVIB1tQ2x/Wv/ZWa/u5PafYhgWeqoAAt2JOHNtH2HNay6lcKpWFQl/ZoQgQqwQMafwYo4ALIcuwx7GlgWcvG3WGBlD5fkGaUBC69RBzSlhsXfu0tbHOI9WOZHEViBmFt62fTRngLvHg+s+kMUgXWpDtExhKADK8zqdg+m3IsIlj6PIrAsNjIYcycFW3PiFLwUod1yF2gaFQkyIwRxuC38zxlRXZL3L0SwckZJ9ZjqGMf94F5dWStzDgnRF5CEOxDBuvYldWBlIS8QWM4S0iCQHsajCrQzkzqwrMgrT9ihRUxVJPEXiFSl/AHaYKkDC2RxpIkdWohS/DbucVU8R8KHlRKssCu4a+XEQ8tnq+D1+SThTlywGk9QChZEmwYjL8KcGuSbHWECyX0dl6qkXxFrO6VggSyKRF/de7zQ98bdNhzFpQq0aJa0jywxWKH4y8af8tc36H3p2OKaScp96GA1h1INFtyG8OKx2YKV5j50CUJrAzZVyfeKmSKawQKB1TfYYIHCqGaf4Aq1oM+hV5ZJ/uDSgwWOmwxgPbJPB5lvjVMF9ceQvEMH6w5iKFQBWCDTgo0ysPXcIX29TrvGlqWBpD0gx3EFyUcEQQEr9IpVBrBAYTmURZPHFm8il1+SgyrRbA9XDVhQTDw8UC8PW/PPm7RWuwytXfkTZKIqY7CEaRx0sAjmHpTeukBLbAk2UjRbJqpAa/cgfQ4ssOBNv3hULxtbsH1OC3cinFXYCUFXTXuI8GaVgUWQC+F76xG1L6CDd4zX1OW+vP0g3qdBBAuOkGFyWVpjTxvU3YwP4/lyXpGVqozHxANSjWARzLEOrvrMQZ1R1TXxuhxxiJ6cVEldyyA3WHCK4PWydtXS7NHV9YhmXd9B6g6ohqo6f1mioN01a5joJagXLJC8Jh7qqPDAyugRf28O6apbKpgspm8pv/4KJsqNlKh3ks4M7A83SIYvcFUMVi3NkfzuVoI+l1z8jfMbTP0jaTguySQCyd0/8SZKuV8JqqDXeYkMn1AOsJoMwpMIJQ+re3RIWxpF/7n395j7KkYuzHuBThsY8qkIUvbCdq5FI2CBfFfM4bqB/edAEn9KSuYR01WFkYIHKHlXfBilqBJDDIfl+ayDZPtWJSwuHdLbDSyZe+vvNPHnpHSheGDIL4YiUrpIfAAFkQKFTJFchoF8YLUYBahHQHEDq7feXpUI5PPBxhfwR7rBj7gRTHL/idtl6ukleL/oLsglg+T8vY2osCK6gV6UTYIZCyOBJI8TwviDtnhStlj8EYrz5Ph1ajkn57seJPOdADOJBkLV4bx+3UDvFIaYQS6ldr8YqPR6JAb8hzCnBQo+C6aQ5Lup4QmxRpQusMw2Mu6Ml5ssVsaYPHIDB9QFdZc45rr0PfF6hbkuHSnEUCLeINZWYtPf9BKMxFIv/mFHGmk6JU7xh2Pv8t8VCHLeVjiUN2ocLDHUrBOevX1jq5cbaJavFE7VCsE883X537ICYIFk1ttuay8ruIGGHoYQ+HcMGk8c4fZERV6xMmCBHMzlBuAGbmHQeKTK5UwVAwvkixSLV25gKPoUA21oxVoFX66SYAkeLHg6kMP1jDRe/B2DxoNFS/OUTZIqCRa5uXygn+FHbtxA+euW1KiF07GrYmgHyx6AeMtdH+LoHm4gRLFhQiaD5pYKgX5e+QHmyoMFAom/Hts0b2YDhdvOBjLNe5PwVOziowIscrOrZ0mUyeEG1gwoG+irCuNDeFrGpdAClt3esid8mBvojcLCVYGiDjiKwCI3a+R7UiVJNlDzWr6KtkJZusDqdUE2k/Q/M276ja3/jNTspvDVUQwWcwM9qTOG+hwqhWKwzLUkayijp++ahaHEXE3t26P7KoR4TPlKKsov6dI7xPoqnur9QnSDZZfWaLH2g/HUdf3dR5rD6H9pagBLvBZrSN5oRpXYDSFj3boPgGUXBZs8abDTxc4t1Uw+URVYYgCi5eYGLJ+yuu4Uq59l6TL1YbDs0pYg1nH7hOs3TIY5CwysbmEusZcBRmhqFSn4aA3HkAaEMrBuSRdHavdqzWeE1qO6ANSpaAwsD8NdRrG4O+MJ1SOV/og4bZaCaioGVje+xDqInH+oMuApdv2HUlWewMDqJbpscuV90T+nH6nke8iV5cRQrL2XoEWwuswvG2mNIUUzcPcoe711Emb5wVA4Oqo9GVheibWNNJ0Wu1Zk2Pp3yzEkAHrjya5ufU2LD4DlaoR1pJOqDeIaXBgEItPh9Evxx0GHbWememMHDKzbuShhOAz4klDRmz1cyvkwcM1lv0jK/MSQAeyFk2EKFwOLaoE1bhDQh+HYlZ+SsqXiYCNwMC89LV6gyb//Ue8WBz3CP6Q/SjKfE/9C/jgRzaqNYjCzPYmYrnk/CImBxYQJA4uJMvJ/KZzDcnj6uUcAAAAASUVORK5CYII=", + "description": "Displays the latest values of the attributes or time-series data for multiple entities in a doughnut chart. Supports numeric values only.", + "descriptor": { + "type": "latest", + "sizeX": 7, + "sizeY": 5, + "resources": [ + { + "url": "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js" + } + ], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showTooltip = utils.defaultValue(settings.showTooltip, true);\n \n Chart.defaults.global.tooltips.enabled = settings.showTooltip;\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\nself.onDestroy = function() {\n self.ctx.chart.destroy();\n self.ctx.chart = null;\n}\n\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-doughnut-chart-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + }, + "externalId": null, + "tags": [ + "ring", + "circle", + "pie chart" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/horizontal_doughnut.json b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json new file mode 100644 index 0000000000..1eeffcae6e --- /dev/null +++ b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json @@ -0,0 +1,29 @@ +{ + "fqn": "horizontal_doughnut", + "name": "Horizontal doughnut", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMkAAACgCAMAAACR47ilAAABp1BMVEX5+fn9/f3///8AAAD29vby8vL29vb///////8Ihyv/X2r+/f3z8/P4+Pjn5+f6+vr/kJjs7OwhISHx8fHr6uru7u78/PzPzs7CwsK2trbIyMiDw5X/2dva2tq8vLyrq6v19fUnlkUYjjj29vbg8OWRkZH39/d0vIfv9/KTy6L/7O3B4crb29v/h49XV1eenp6i0q//9fXg8OQ9PT3w8PDp6en/govW1dX/+/wnlkbj4+P/293/w8f/4eP/2tzR6dhFpWDi4eGEhISy2b1GpWBks3r/r7T/paz/+Pn/5uj/3uDU1NT/ub4vLy//sbf/c33/TVr/6ev/m6IXjjj/9vf/8vOwsLBtbW1hYWFKSkr/3N//aXP/7/H/4+XCqKp0dHRmZmb/zdHYysv/jZb/ipM1nVL/4OLh4OCkpKR5eXlVrG3w9/LnyMrLurudnZ3/fYb23+H/sLbNsLGi0rC8oaM8PDzj0tPz0NPfwsPZvL6/tbZVrW03nVLn2dr51tjLycnPxMXDvr//aXTz6OjZ6d272sPOv8Ch0a/ymZ+Pxp6knp62nJ1+vpCq9c7pAAAACHRSTlODge8AU0N0cBMSxpIAAA11SURBVHja7JqHjxJBFMat8dOdGRx3ly1U5eAQF/EU7O1sqJwae9fYezf2rrH/0c4C50NR987M2uIv4TLkkgu/fO/tmxluwuRpk/D3M2ni5AlT8G8wdcJE/BtM/G/yxzFxwgT8G0z4Z0z+Z/Ln8d/kz+O/yZ/Hf5M/j/8mhGHAULQXvxD9JoaRTLRJGr/QRb+JYSQ4LxXy+UKBc2WTNBSIHf0mKo8SM21b2nbdNFm+FNoolZhl9JsYBi+Y0qozW1pCWFLpsEJJycQXjGaTgfVzBzcODqjSKtStAjpwZluiIW0lo5LR7aLfpLx+cMOc6R3eJEqmTKLS3NUcObEKCqNgN9oyJa4zGP0m81Ibpvew22aSYeWLGR2qzRMVANy2QpmCLhf9JgOp2dO/4p1poaJEiGvNNABeF5Zt5tsu0IBWk3mUBrGxXsfIjK/YsasCJG3LkqGLBhWdJuVU2Br9fLRN7JrRT3UlkMg3LJuVNLjoMiGPfl7VTcrky2A2VZAwhVVnGkpMl8kC8viKFA/7pF+DXGwhzYKGWDSYUH/0MWeuwZnk2DTjey4jQFKEJaZiQaxEm5QHvykxe2NqwRpADcZwnqzc8V2XE4AZxhJzhUWbzFv2DYvB9WV0MJI8bysVVNIjzeo3XdRzLNmwzHysKtEmqT6NtakBEIaRUCqWyQ2EpEfIhmJZCcNWFcZjU4k2KW/4ujEG56GXroppSyFknfHti2fOPHX6dLVaXdxLtXr69KlTM8dKU7vJwLKvPFJl9GGELgVWt6Vlvd0OGEs3Dx2pbDm6auuKFavTIasPnd1S2btn6ZgD2X4DhA6TeXN+7EEqyUR41GLmKSiWKpW9R3ZuWbX1UEdl9YpDq8anUgWhwWRutAe5KJkE5zPRVdkztLeiXDqxKJWt41OZSUsNJqkv+3whiG/LqNdMKLhgqsJUid3ZefSsimV1pIpwAAQifOk1IRGagIiAPgHzAjcDQ+Xy/EilW2JK5Qe94h1Ytx8HivM9r7YfnkaTfpEzCxEFmUgXfJg7/rmlz/ecvPLg+r1j91anbx67fffslSvP9jw1HC4zzHWZcJegQ8BwkOXgFXPehZrQbHKrV2QQ0ZAJnGwGmWF43F0aDJ08eeWySuV2+u7te/e3XHn8YKnXagmPMTcTgEIpAmy+gOvUoNdkTa/ImCqLTDjHEsdlcIQLx39w8vUd1fjHVqc/3Lx89uTJk08zWRf7XNfJCFCjzGc44AG4UCzm9Jj0z5E58zAWyET4XLiBw1Um8Ex/ybPHO49uvX338rHLN58cu379fd73kZV8mEyKRWc+W3fBCeC569g6jSblHpHZazAmyATD/hKOwJc4p5YZa/jlkZ1Hj92/+SR9/9jlVVvuDFkWVBsxydCFecUAtVrNAyCKQqPJYI9IVK9Hz4Fw5O88trU9WNSI3HlkaLOBCDSZLIgQGfsnIJXrR5VKe64cpWdxbCb9TTJnDcbHYnxHRQ2WQ51pr8YKhRKvSc/2dz0iiNj6kcreigolrWiHsnlpxJ9pYvyQSX9tpTBumjMJor3Rpx1+1Aa/uh3jh0z6a2sjtGAYPG8LEyfo6LUJpYbJtR7s+0029na7HsIDshSJ3muLVZCS6VTpNxmg2poHXahUWKhCB+MquKiXEnGaUCSD0IeR4KZloXL4s0oadhyhkAlFMrsMfRhGomQ2GEboFhyJMBRohkwokgXQiVIp2F/U18owlERsmVAkG6AX1fVMqvrq7ZSGqbtTyIQiWQjNqPqqiwKa1CmQksVlMqB3lBDd55fdMIYO94RiqZkCjZDJgrgi6daX2TCxiUKBsPNaQyGTZXF1Sbe+mC16QmnCtJjmLyO6Jmto5wjNUCjUKYeREGYpGYMJHbBmIxZUKFJgVW/Pq/KKwYSKK4VYCMejKNFM2RWWF4/BZGA8/T7stMHXsBq+i6FCsSyMxFNeZEJPrkeIRgTBeScIRt95PzahUL7o+TQadiGh3+ThOO+3cgIAD1oMzDkfAKI1HGECg6t6ovJqoi5ZDCZzRk3WIAIyYTnfyQnpnnfg5Ja4XpSJ6nmJlTQc8xbjuk3oGbwbUZCJ6wItDyIHCAbUWIRJMm8vN4ZmjDKU0Hd0JJP1oyYXEQWZeJ2CCk2Yv8+LNlEjpYAdvWO+oN1kcOxtQibZDCDPt028JdGZwODMsmk4jkDKfAJtynPnDiDEAVzAFxl0yGTwA5yrQG7/OibW7Xe7Jmvp1Dt2E8dTL1eZtN+KaBPVKBY1yi7YVtjy3ZuQZWUofMnOS+lzxphg4MJRJmrJAckgwds/BCAlgNy6+QjUXbK3LicOdk1mj5qUx2ECP7fPY+C5HJbUct55EWlSsHvG/DXkR7//nvu5HM61Mk4rM5zJOG6QVfUb5u74S7LIDrvCZy0fWaZ+w/YFUIj5ANwDAhAHuibU8OOCM4Sw9ioS1Sh1kUDPbBzd2ZMJd11kXaZMBDzmt6tLLV2RBXdFy8+KwHGdfczFZ5Pa/owSER2TNbQPjgmaKNTyQ+0pD8Wl2aP3UkoDfhZdE3fUJMuyYC73/NY+1goYIxPholZjBxx0TOZFPLq0msyke6/Pl0UL587t1rUfION2TeC77era5/twXI8hK/g+MM/xyYTNv3BAFA/u3x+0TdZrux2KfHhJ+s+2E9H7FQ6FI3h32YWBoHdtkwUxb4R7pzwdHFeO8dw4LDEmQpOUnuuhaJO8tDBCJhaZaOCnTMzl6ODncj46LFdLE9+EHsOyQQNlEyzJfq+Jf3xbESHL589SLAq1zM6yhh+hTMSXJr8zk+WfiDmb3qaBIAxf38DuBq+T+APiiIaASlqj1EIWFlLaILiAA1UlIKeGA0KABEKIG1f+N17bdBQlLhh2xCu13aa9PNr52JlZe5JrrSuSROeLyTeduMDKLFdah9ghsq4Nkv9sXSOt47wiCbWOyu9hvUSsz7GlZhLH+p7styAJP7lIKpJIa/ND6xHSSfnJuY6xU+TxnNbVPgrXJGlpTBmZ1G/3ZFzGLjoMM5C0yIxEgrVOomilV3U8Sxc6z7BDtCeb+aTXH1smmbZqCRMJyoC1cn85UCMIZcY+5fgbBUl17sJsuTyFkTDnEyM1xJ9p9OpVAG9SkxxRZ6UVyUTn67XWizo2r4swdhnKQIqei8ONc1dZNJ5eefHiygyFzOFKwhGGxKnrEOkoAfObkKjqEwHhSFR65SnhTRL83ak+uYhdKZDleoRK2epyjy/L3890FlZ1fbJ8UehL6XqhAPwT/0QN34UHEUYpoKKDQIaeqVtUgEgEJ4Ecpb9IXr9fCCDBX1RaRLLQcRWw1qj1SeeXgAyKKYoE1Sfy/hYJlB/KCDJSQzwNXyKoDM1TEVDg+L7jvwu8wPGpAhaTe0RC7a5pG5LzytfXBdC9JK5INHaIencZ1YyHVDPOjHWd1nV8qCKIgmR4gIBIAiCAH6pAvfOEkBck5XU9ImnVkSCST5V1GUcJTVKBm2xbV2M6uQZq3Z0ulzMYpX4YlbWIGqrAr/ckCqPiD9EJvAOMIAMvIhJM3seKSCgMf2hD4n7TOl7n+lsGxNVS7/Z46q1Q6Hq2OzHK+osqEjUsP5Mb/9JQn1DwOmtDguxcF4ozQ7XIzTJFswbk8NTvwu8lFP5AW93UKdopzdyLZeqiQdTipgHwbKBsjhqpw81bNW63uw4x7rn2+8LYZ++uXDXZROAa9er7HL16cpTOLbDIZJO+6s6ubLhJF/ZUk+Amq3nR0IF9poXrFL14ZGLwxpxR2HWTC5Kp7atdJJo5gCLXmzKbcJDgmMvnqSns0CXC5xioPs88HteZN6UrXXIThiE2kXzcY90UY109Oj7exX22eyu4w7gp9R0viUOutAgiIZ/nuoMz7vdx93npJXfLux5cJDjm3xQBPLx27ckMwnFlF1ZFJLQpZuZnX1eNz7uDcun23DHXPUjaFK45ytGds87b/V5fCNfpm1uQjCTzDqN93dqr39dg3jEjGECIhMIXg33Rlf0ft8dj2WUAIRLKKQwTR7r5+rjL9YocIkFZpvCciekEcXzVCAzaIMExoXyFPT3q8AWTBpIjsq+zOWxpvmf5cRBSE8mGfZ3ZKR/J21lnspc+BfjABgqB8NhWM8nH75ZR5gaE4XGQ35Lg6GbHpq9M9zq8TtJMYvyTtI9/kvE70hx8anqq3E5eIa+juM6lxif9rTjLrZ/t3TGKwzAQheFAdpfRGVRJCGShxqTbygIlJ9gzqMnev1/MFoMjJ/IEJhmMvhPkj2zUPZfqP2HUXl/4CfCcYF8Ysm0R4+yAzp3VK0Mer5QgTWupN78CsMKSWl7+ku8RNqs7bAQm7RK8V7DFwRbYwXOP0EvglNSSvUzQFr1VS2kAftTVqxIcPOJwMJLxXaeXzE9YpeAS2dKQvVEVE4GEqQR+vVpTrjpHPB03jfpi1Bo/ABFTCR7LGmtmVt2VIjAglqARW2jsCJzoJeQW3PxiRi+ZW6R3YEnLmNQtIe9HVdJ08kZtYbQDVvSS2qRT6zR0hLd4ar06WbXGJrwzebRLyKYcfErmv8iYcvUhO3ir/l0HiXqJPL1Enl4iTy+Rp5fI00vk6SXy9BJ59lTyCfvwcTgedvHV9a/jH7TmXldOIe3fAAAAAElFTkSuQmCC", + "description": "Displays the latest values of the attributes or time-series data in a doughnut chart using horizontal layout. Supports numeric values only.", + "descriptor": { + "type": "latest", + "sizeX": 4, + "sizeY": 2.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.doughnutWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.doughnutWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '380px',\n previewHeight: '220px',\n embedTitlePanel: true,\n horizontal: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind power', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar power', type: 'timeseries', color: '#FF4D5A' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-doughnut-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-doughnut-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"totalValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1\"},\"totalValueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showLegend\":true,\"legendPosition\":\"right\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.38)\",\"legendValueFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"legendValueColor\":\"rgba(0, 0, 0, 0.87)\",\"showTooltip\":true,\"tooltipValueType\":\"percentage\",\"tooltipValueDecimals\":0,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + }, + "externalId": null, + "tags": [ + "ring", + "circle", + "pie chart", + "donut" + ] +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index d49b2b04df..002b27d265 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -105,6 +105,7 @@ import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARA import static org.thingsboard.server.controller.ControllerConstants.USER_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.USER_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; +import static org.thingsboard.server.dao.entity.BaseEntityService.NULL_CUSTOMER_ID; @RequiredArgsConstructor @RestController @@ -439,32 +440,28 @@ public class UserController extends BaseController { @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - checkParameter("alarmId", strAlarmId); - AlarmId alarmEntityId = new AlarmId(toUUID(strAlarmId)); - Alarm alarm = checkAlarmId(alarmEntityId, Operation.READ); - SecurityUser currentUser = getCurrentUser(); - TenantId tenantId = currentUser.getTenantId(); - CustomerId originatorCustomerId = entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator()).get(); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - PageData pageData; - if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { - if (alarm.getCustomerId() == null) { - pageData = userService.findTenantAdmins(tenantId, pageLink); - } else { - ArrayList customerIds = new ArrayList<>(Collections.singletonList(new CustomerId(CustomerId.NULL_UUID))); - if (!CustomerId.NULL_UUID.equals(originatorCustomerId.getId())) { - customerIds.add(originatorCustomerId); - } - pageData = userService.findUsersByCustomerIds(tenantId, customerIds, pageLink); - } + checkParameter("alarmId", strAlarmId); + AlarmId alarmEntityId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmEntityId, Operation.READ); + SecurityUser currentUser = getCurrentUser(); + TenantId tenantId = currentUser.getTenantId(); + CustomerId originatorCustomerId = entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator()).orElse(NULL_CUSTOMER_ID); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData pageData; + if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { + if (alarm.getCustomerId() == null) { + pageData = userService.findTenantAdmins(tenantId, pageLink); } else { - pageData = userService.findCustomerUsers(tenantId, alarm.getCustomerId(), pageLink); + ArrayList customerIds = new ArrayList<>(Collections.singletonList(NULL_CUSTOMER_ID)); + if (!CustomerId.NULL_UUID.equals(originatorCustomerId.getId())) { + customerIds.add(originatorCustomerId); + } + pageData = userService.findUsersByCustomerIds(tenantId, customerIds, pageLink); } - return pageData.mapData(user -> new UserEmailInfo(user.getId(), user.getEmail(), user.getFirstName(), user.getLastName())); - } catch (Exception e) { - throw handleException(e); + } else { + pageData = userService.findCustomerUsers(tenantId, alarm.getCustomerId(), pageLink); } + return pageData.mapData(user -> new UserEmailInfo(user.getId(), user.getEmail(), user.getFirstName(), user.getLastName())); } @ApiOperation(value = "Save user settings (saveUserSettings)", diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 812ca137aa..b04291a303 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -301,7 +301,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene if (subInfo != null) { log.trace("[{}][{}] Handling alarm update {}: {}", tenantId, entityId, alarm, deleted); for (Map.Entry entry : subInfo.getSubs().entrySet()) { - if (entry.getValue().notifications) { + if (entry.getValue().alarms) { onAlarmSubUpdate(entry.getKey(), entityId, alarm, deleted); } } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 0c29b19fd9..3505052fca 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -192,7 +192,7 @@ public class DefaultTransportApiService implements TransportApiService { final String certChain = msg.getCertificateChain(); result = handlerExecutor.submit(() -> validateOrCreateDeviceX509Certificate(certChain)); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { - result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); + result = handlerExecutor.submit(() -> handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg())); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { result = handle(transportApiRequestMsg.getEntityProfileRequestMsg()); } else if (transportApiRequestMsg.hasLwM2MRequestMsg()) { @@ -223,7 +223,6 @@ public class DefaultTransportApiService implements TransportApiService { } private TransportApiResponseMsg validateCredentials(String credentialsId, DeviceCredentialsType credentialsType) { - //TODO: Make async and enable caching DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsId); if (credentials != null && credentials.getCredentialsType() == credentialsType) { return getDeviceInfo(credentials); @@ -336,76 +335,74 @@ public class DefaultTransportApiService implements TransportApiService { return VALID; } - private ListenableFuture handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) { + private TransportApiResponseMsg handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) { DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB())); - ListenableFuture gatewayFuture = deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, gatewayId); - return Futures.transform(gatewayFuture, gateway -> { - Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(requestMsg.getDeviceName(), id -> new ReentrantLock()); - deviceCreationLock.lock(); - try { - Device device = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), requestMsg.getDeviceName()); - if (device == null) { - TenantId tenantId = gateway.getTenantId(); - device = new Device(); - device.setTenantId(tenantId); - device.setName(requestMsg.getDeviceName()); - device.setType(requestMsg.getDeviceType()); - device.setCustomerId(gateway.getCustomerId()); - DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); - - device.setDeviceProfileId(deviceProfile.getId()); - ObjectNode additionalInfo = JacksonUtil.newObjectNode(); - additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); - device.setAdditionalInfo(additionalInfo); - Device savedDevice = deviceService.saveDevice(device); - tbClusterService.onDeviceUpdated(savedDevice, null); - device = savedDevice; - - relationService.saveRelation(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); - - TbMsgMetaData metaData = new TbMsgMetaData(); - CustomerId customerId = gateway.getCustomerId(); - if (customerId != null && !customerId.isNullUid()) { - metaData.putValue("customerId", customerId.toString()); - } - metaData.putValue("gatewayId", gatewayId.toString()); + Device gateway = deviceService.findDeviceById(TenantId.SYS_TENANT_ID, gatewayId); + Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(requestMsg.getDeviceName(), id -> new ReentrantLock()); + deviceCreationLock.lock(); + try { + Device device = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), requestMsg.getDeviceName()); + if (device == null) { + TenantId tenantId = gateway.getTenantId(); + device = new Device(); + device.setTenantId(tenantId); + device.setName(requestMsg.getDeviceName()); + device.setType(requestMsg.getDeviceType()); + device.setCustomerId(gateway.getCustomerId()); + DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); + + device.setDeviceProfileId(deviceProfile.getId()); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); + device.setAdditionalInfo(additionalInfo); + Device savedDevice = deviceService.saveDevice(device); + tbClusterService.onDeviceUpdated(savedDevice, null); + device = savedDevice; + + relationService.saveRelation(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); + + TbMsgMetaData metaData = new TbMsgMetaData(); + CustomerId customerId = gateway.getCustomerId(); + if (customerId != null && !customerId.isNullUid()) { + metaData.putValue("customerId", customerId.toString()); + } + metaData.putValue("gatewayId", gatewayId.toString()); - DeviceId deviceId = device.getId(); - JsonNode entityNode = JacksonUtil.valueToTree(device); - TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); - tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); - } else { - JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); - if (deviceAdditionalInfo == null) { - deviceAdditionalInfo = JacksonUtil.newObjectNode(); - } - if (deviceAdditionalInfo.isObject() && - (!deviceAdditionalInfo.has(DataConstants.LAST_CONNECTED_GATEWAY) - || !gatewayId.toString().equals(deviceAdditionalInfo.get(DataConstants.LAST_CONNECTED_GATEWAY).asText()))) { - ObjectNode newDeviceAdditionalInfo = (ObjectNode) deviceAdditionalInfo; - newDeviceAdditionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); - Device savedDevice = deviceService.saveDevice(device); - tbClusterService.onDeviceUpdated(savedDevice, device); - } + DeviceId deviceId = device.getId(); + JsonNode entityNode = JacksonUtil.valueToTree(device); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); + } else { + JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); + if (deviceAdditionalInfo == null) { + deviceAdditionalInfo = JacksonUtil.newObjectNode(); } - GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() - .setDeviceInfo(getDeviceInfoProto(device)); - DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); - if (deviceProfile != null) { - builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); - } else { - log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId()); + if (deviceAdditionalInfo.isObject() && + (!deviceAdditionalInfo.has(DataConstants.LAST_CONNECTED_GATEWAY) + || !gatewayId.toString().equals(deviceAdditionalInfo.get(DataConstants.LAST_CONNECTED_GATEWAY).asText()))) { + ObjectNode newDeviceAdditionalInfo = (ObjectNode) deviceAdditionalInfo; + newDeviceAdditionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); + Device savedDevice = deviceService.saveDevice(device); + tbClusterService.onDeviceUpdated(savedDevice, device); } - return TransportApiResponseMsg.newBuilder() - .setGetOrCreateDeviceResponseMsg(builder.build()) - .build(); - } catch (JsonProcessingException e) { - log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); - throw new RuntimeException(e); - } finally { - deviceCreationLock.unlock(); } - }, dbCallbackExecutorService); + GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() + .setDeviceInfo(getDeviceInfoProto(device)); + DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); + if (deviceProfile != null) { + builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); + } else { + log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId()); + } + return TransportApiResponseMsg.newBuilder() + .setGetOrCreateDeviceResponseMsg(builder.build()) + .build(); + } catch (JsonProcessingException e) { + log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); + throw new RuntimeException(e); + } finally { + deviceCreationLock.unlock(); + } } private ListenableFuture handle(ProvisionDeviceRequestMsg requestMsg) { diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index adec1951dd..d280c5770c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -121,18 +121,47 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { loginSysAdmin(); + ObjectNode config = JacksonUtil.newObjectNode(); + + ObjectNode http = JacksonUtil.newObjectNode(); + http.put("enabled", true); + http.put("host", ""); + http.put("port", 8080); + config.set("http", http); + + ObjectNode https = JacksonUtil.newObjectNode(); + https.put("enabled", true); + https.put("host", ""); + https.put("port", 444); + config.set("https", https); + + ObjectNode mqtt = JacksonUtil.newObjectNode(); + mqtt.put("enabled", true); + mqtt.put("host", ""); + mqtt.put("port", 1883); + config.set("mqtt", mqtt); + + ObjectNode mqtts = JacksonUtil.newObjectNode(); + mqtts.put("enabled", true); + mqtts.put("host", ""); + mqtts.put("port", 8883); + config.set("mqtts", mqtts); + + ObjectNode coap = JacksonUtil.newObjectNode(); + coap.put("enabled", true); + coap.put("host", ""); + coap.put("port", 5683); + config.set("coap", coap); + + ObjectNode coaps = JacksonUtil.newObjectNode(); + coaps.put("enabled", true); + coaps.put("host", ""); + coaps.put("port", 5684); + config.set("coaps", coaps); + AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class); - JsonNode connectivity = adminSettings.getJsonValue(); - - ((ObjectNode)connectivity.get("http")).put("port", 8080); - ((ObjectNode)connectivity.get("http")).put("enabled", true); - ((ObjectNode)connectivity.get("https")).put("enabled", true); - ((ObjectNode)connectivity.get("https")).put("port", 444); - ((ObjectNode)connectivity.get("mqtt")).put("enabled", true); - ((ObjectNode)connectivity.get("mqtts")).put("enabled", true); - ((ObjectNode)connectivity.get("coap")).put("enabled", true); - ((ObjectNode)connectivity.get("coaps")).put("enabled", true); - doPost("/api/admin/settings", adminSettings); + adminSettings.setJsonValue(config); + doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); Tenant tenant = new Tenant(); tenant.setTitle("My tenant"); @@ -212,19 +241,19 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { JsonNode mqttCommands = commands.get(MQTT); assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " + - "-u %s -m \"{temperature:25}\"", + "-u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + - "-t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", credentials.getCredentialsId())); + "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + - " -p 1883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", + " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + - "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"\"", + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", credentials.getCredentialsId())); JsonNode linuxCoapCommands = commands.get(COAP); @@ -251,18 +280,18 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { JsonNode mqttCommands = commands.get(MQTT); assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + - "-u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + "-u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + - "-t %s -u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + "-t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + - " -p 1883 -t %s -u %s -m \"{temperature:25}\"", + " -p 1883 -t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + - "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -u %s -m \"{temperature:25}\"\"", + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); } @@ -295,18 +324,18 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { JsonNode mqttCommands = commands.get(MQTT); assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + - "-i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + "-i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + - "-t %s -i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + "-t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + - " -p 1883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"", + " -p 1883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + - "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"\"", + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java index 0889f458ba..f2f52443c3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java @@ -29,6 +29,7 @@ import org.mockito.Mockito; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Device; @@ -66,16 +67,47 @@ public class DeviceConnectivityControllerWithDefaultPortTest extends AbstractCon loginSysAdmin(); + ObjectNode config = JacksonUtil.newObjectNode(); + + ObjectNode http = JacksonUtil.newObjectNode(); + http.put("enabled", true); + http.put("host", ""); + http.put("port", 80); + config.set("http", http); + + ObjectNode https = JacksonUtil.newObjectNode(); + https.put("enabled", true); + https.put("host", ""); + https.put("port", 443); + config.set("https", https); + + ObjectNode mqtt = JacksonUtil.newObjectNode(); + mqtt.put("enabled", false); + mqtt.put("host", ""); + mqtt.put("port", 1883); + config.set("mqtt", mqtt); + + ObjectNode mqtts = JacksonUtil.newObjectNode(); + mqtts.put("enabled", false); + mqtts.put("host", ""); + mqtts.put("port", 8883); + config.set("mqtts", mqtts); + + ObjectNode coap = JacksonUtil.newObjectNode(); + coap.put("enabled", false); + coap.put("host", ""); + coap.put("port", 5683); + config.set("coap", coap); + + ObjectNode coaps = JacksonUtil.newObjectNode(); + coaps.put("enabled", false); + coaps.put("host", ""); + coaps.put("port", 5684); + config.set("coaps", coaps); + AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class); - JsonNode connectivity = adminSettings.getJsonValue(); - - ((ObjectNode) connectivity.get("http")).put("port", 80); - ((ObjectNode) connectivity.get("https")).put("enabled", true); - ((ObjectNode) connectivity.get("mqtt")).put("enabled", false); - ((ObjectNode) connectivity.get("mqtts")).put("enabled", false); - ((ObjectNode) connectivity.get("coaps")).put("enabled", false); - ((ObjectNode) connectivity.get("coap")).put("enabled", false); - doPost("/api/admin/settings", adminSettings); + adminSettings.setJsonValue(config); + doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); Tenant tenant = new Tenant(); tenant.setTitle("My tenant"); diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index 28bb987257..d37b4ebe5b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.settings.StarredDashboardInfo; import org.thingsboard.server.common.data.settings.UserDashboardsInfo; +import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.user.UserDao; @@ -81,6 +82,9 @@ public class UserControllerTest extends AbstractControllerTest { @Autowired private UserDao userDao; + @Autowired + private DeviceService deviceService; + static class Config { @Bean @Primary @@ -740,6 +744,43 @@ public class UserControllerTest extends AbstractControllerTest { Assert.assertEquals(expectedCustomerUserIds, loadedUserIds); } + @Test + public void testGetUsersForDeletedAlarmOriginator() throws Exception { + loginTenantAdmin(); + + String email = "testEmail1"; + for (int i = 0; i < 45; i++) { + User customerUser = createCustomerUser( customerId); + customerUser.setEmail(email + StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)) + "@thingsboard.org"); + doPost("/api/user", customerUser, User.class); + } + + Device device = new Device(); + device.setName("testDevice"); + device.setCustomerId(customerId); + Device savedDevice = doPost("/api/device", device, Device.class); + + Alarm alarm = createTestAlarm(savedDevice); + + deviceService.deleteDevice(tenantId, savedDevice.getId()); + + List loadedUserIds = new ArrayList<>(); + PageLink pageLink = new PageLink(33, 0); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/users/assign/" + alarm.getId().getId().toString() + "?", + new TypeReference<>() {}, pageLink); + loadedUserIds.addAll(pageData.getData().stream().map(UserEmailInfo::getId) + .collect(Collectors.toList())); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(1, loadedUserIds.size()); + Assert.assertEquals(tenantAdminUserId, loadedUserIds.get(0)); + } + @Test public void testDeleteUserWithDeleteRelationsOk() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java index 80d70c1b8b..82dfa82d5a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java @@ -268,6 +268,13 @@ public class WidgetsBundleControllerTest extends AbstractControllerTest { Collections.sort(loadedWidgetsBundles2, idComparator); Assert.assertEquals(tenantWidgetsBundles, loadedWidgetsBundles2); + + // cleanup + loginSysAdmin(); + for (WidgetsBundle sysWidgetsBundle : sysWidgetsBundles) { + doDelete("/api/widgetsBundle/" + sysWidgetsBundle.getId().getId().toString()) + .andExpect(status().isOk()); + } } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 05eacc68e3..6acf71e2bb 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -458,7 +458,7 @@ public class TbRuleEngineQueueConsumerManagerTest { verify(consumer2, never()).unsubscribe(); int msgCount = totalConsumedMsgs.get(); - await().atLeast(4, TimeUnit.SECONDS) // based on topicDeletionDelayInSec + await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) .atMost(7, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() @@ -498,7 +498,7 @@ public class TbRuleEngineQueueConsumerManagerTest { verify(consumer, never()).unsubscribe(); int msgCount = totalConsumedMsgs.get(); - await().atLeast(4, TimeUnit.SECONDS) + await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) .atMost(7, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java index 9e6ba806f1..475b268f20 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java @@ -34,6 +34,10 @@ public class DeviceCacheKey implements Serializable { private final DeviceId deviceId; private final String deviceName; + public DeviceCacheKey(DeviceId deviceId) { + this(null, deviceId, null); + } + public DeviceCacheKey(TenantId tenantId, DeviceId deviceId) { this(tenantId, deviceId, null); } @@ -44,11 +48,12 @@ public class DeviceCacheKey implements Serializable { @Override public String toString() { - if (deviceId != null) { - return tenantId + "_" + deviceId; - } else { + if (deviceId == null) { return tenantId + "_n_" + deviceName; + } else if (tenantId == null) { + return deviceId.toString(); + } else { + return tenantId + "_" + deviceId; } } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 8679ad885a..24d9c6366b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -188,6 +188,7 @@ public class BaseAlarmService extends AbstractCachedEntityService { - //TODO: possible bug source since sometimes we need to clear cache by tenant id and sometimes by sys tenant id? - if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - return deviceDao.findById(tenantId, deviceId.getId()); - } else { - return deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()); - } - }, true); + if (TenantId.SYS_TENANT_ID.equals(tenantId)) { + return cache.getAndPutInTransaction(new DeviceCacheKey(deviceId), + () -> deviceDao.findById(tenantId, deviceId.getId()), true); + } else { + return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, deviceId), + () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()), true); + } } @Override @@ -258,6 +256,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService keys = new ArrayList<>(3); keys.add(new DeviceCacheKey(event.getTenantId(), event.getNewName())); if (event.getDeviceId() != null) { + keys.add(new DeviceCacheKey(event.getDeviceId())); keys.add(new DeviceCacheKey(event.getTenantId(), event.getDeviceId())); } if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index d6315a2eca..db516388a4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -54,20 +54,20 @@ public class DeviceConnectivityUtil { switch (deviceCredentials.getCredentialsType()) { case ACCESS_TOKEN: - command.append(" -u ").append(deviceCredentials.getCredentialsId()); + command.append(" -u \"").append(deviceCredentials.getCredentialsId()).append("\""); break; case MQTT_BASIC: BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); if (credentials != null) { if (credentials.getClientId() != null) { - command.append(" -i ").append(credentials.getClientId()); + command.append(" -i \"").append(credentials.getClientId()).append("\""); } if (credentials.getUserName() != null) { - command.append(" -u ").append(credentials.getUserName()); + command.append(" -u \"").append(credentials.getUserName()).append("\""); } if (credentials.getPassword() != null) { - command.append(" -P ").append(credentials.getPassword()); + command.append(" -P \"").append(credentials.getPassword()).append("\"");; } } else { return null; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java index 8ed53e2607..229153e2eb 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java @@ -33,8 +33,8 @@ public class WsClient extends WebSocketClient { private WsTelemetryResponse message; private volatile boolean firstReplyReceived; - private CountDownLatch firstReply = new CountDownLatch(1); - private CountDownLatch latch = new CountDownLatch(1); + private final CountDownLatch firstReply = new CountDownLatch(1); + private final CountDownLatch latch = new CountDownLatch(1); private final long timeoutMultiplier; @@ -48,7 +48,8 @@ public class WsClient extends WebSocketClient { } @Override - public void onMessage(String message) { + public synchronized void onMessage(String message) { + log.error("WS onMessage: {}", message); if (!firstReplyReceived) { firstReplyReceived = true; firstReply.countDown(); @@ -66,12 +67,13 @@ public class WsClient extends WebSocketClient { } @Override - public void onClose(int code, String reason, boolean remote) { - log.info("ws is closed, due to [{}]", reason); + public synchronized void onClose(int code, String reason, boolean remote) { + log.error("WS onClose: [{}]", reason); } @Override - public void onError(Exception ex) { + public synchronized void onError(Exception ex) { + log.error("WS onError: ", ex); ex.printStackTrace(); } diff --git a/msa/black-box-tests/src/test/resources/logback.xml b/msa/black-box-tests/src/test/resources/logback.xml index 0df6c199f4..9d1c96116c 100644 --- a/msa/black-box-tests/src/test/resources/logback.xml +++ b/msa/black-box-tests/src/test/resources/logback.xml @@ -25,6 +25,7 @@ + diff --git a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml new file mode 100644 index 0000000000..e998063b5b --- /dev/null +++ b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml @@ -0,0 +1,58 @@ + + + + + + + /var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.log + + /var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/tb-transports/coap/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-transports/coap/conf/logback.xml new file mode 100644 index 0000000000..397693c501 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/tb-transports/coap/conf/logback.xml @@ -0,0 +1,57 @@ + + + + + + + /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.log + + /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/tb-transports/http/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-transports/http/conf/logback.xml new file mode 100644 index 0000000000..6f9f63383e --- /dev/null +++ b/msa/black-box-tests/src/test/resources/tb-transports/http/conf/logback.xml @@ -0,0 +1,57 @@ + + + + + + + /var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.log + + /var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/tb-transports/mqtt/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-transports/mqtt/conf/logback.xml new file mode 100644 index 0000000000..8d0e650a70 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/tb-transports/mqtt/conf/logback.xml @@ -0,0 +1,55 @@ + + + + + + + /var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.log + + /var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 0c9eca5f0a..1a2984ef4f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -194,7 +194,7 @@ public class TbHttpClient { config.isIgnoreRequestBody()) { entity = new HttpEntity<>(headers); } else { - entity = new HttpEntity<>(getData(msg), headers); + entity = new HttpEntity<>(getData(msg, config.isIgnoreRequestBody(), config.isParseToPlainText()), headers); } URI uri = buildEncodedUri(endpointUrl); @@ -242,12 +242,19 @@ public class TbHttpClient { return uri; } - private String getData(TbMsg msg) { - String data = msg.getData(); + private String getData(TbMsg tbMsg, boolean ignoreBody, boolean parseToPlainText) { + if (!ignoreBody && parseToPlainText) { + return parseJsonStringToPlainText(tbMsg.getData()); + } + return tbMsg.getData(); + } - if (config.isTrimDoubleQuotes()) { + protected String parseJsonStringToPlainText(String data) { + if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) { final String dataBefore = data; - data = data.replaceAll("^\"|\"$", ""); + try { + data = JacksonUtil.fromString(data, String.class); + } catch (Exception ignored) {} log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index 281f65e1e4..a37310331e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.rest; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -23,6 +25,7 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; @Slf4j @@ -30,6 +33,7 @@ import org.thingsboard.server.common.msg.TbMsg; type = ComponentType.EXTERNAL, name = "rest api call", configClazz = TbRestApiCallNodeConfiguration.class, + version = 1, nodeDescription = "Invoke REST API calls to external REST server", nodeDetails = "Will invoke REST API call GET | POST | PUT | DELETE to external REST server. " + "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." + @@ -45,6 +49,8 @@ import org.thingsboard.server.common.msg.TbMsg; ) public class TbRestApiCallNode extends TbAbstractExternalNode { + static final String PARSE_TO_PLAIN_TEXT = "parseToPlainText"; + static final String TRIM_DOUBLE_QUOTES = "trimDoubleQuotes"; protected TbHttpClient httpClient; @Override @@ -72,4 +78,21 @@ public class TbRestApiCallNode extends TbAbstractExternalNode { } } + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0: + if (!oldConfiguration.has(PARSE_TO_PLAIN_TEXT) && oldConfiguration.has(TRIM_DOUBLE_QUOTES)) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put(PARSE_TO_PLAIN_TEXT, oldConfiguration.get(TRIM_DOUBLE_QUOTES).booleanValue()); + ((ObjectNode) oldConfiguration).remove(TRIM_DOUBLE_QUOTES); + } + break; + default: + break; + } + return new TbPair<>(hasChanges, oldConfiguration); + } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 3fb28d474b..1ab4c8d0f8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -37,7 +37,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration data = metaData.getData(); - Assertions.assertThat(data).hasSize(2); - Assertions.assertThat(data.get("Content-Type")).isEqualTo("binary"); - Assertions.assertThat(data.get("Set-Cookie")).isEqualTo("[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]"); + Assertions.assertEquals(2, data.size()); + Assertions.assertEquals(data.get("Content-Type"), "binary"); + Assertions.assertEquals(data.get("Set-Cookie"), "[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]"); } -} \ No newline at end of file + @ParameterizedTest + @ValueSource(strings = { "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt", + "First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]", + "[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c", + "世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"}) + public void testParseJsonStringToPlainText(String original) { + Mockito.when(client.parseJsonStringToPlainText(anyString())).thenCallRealMethod(); + + String serialized = JacksonUtil.toString(original); + Assertions.assertNotNull(serialized); + Assertions.assertEquals(original, client.parseJsonStringToPlainText(serialized)); + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 705f298513..0a71a799fc 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.rest; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -26,6 +27,7 @@ import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import org.junit.After; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -39,6 +41,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -91,7 +94,9 @@ public class TbRestApiCallNodeTest { @After public void teardown() { - server.stop(); + if (server != null) { + server.stop(); + } } @Test @@ -211,4 +216,20 @@ public class TbRestApiCallNodeTest { assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); } + @Test + public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + var defaultConfig = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + var node = new TbRestApiCallNode(); + String oldConfig = "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," + + "\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," + + "\"useSystemProxyProperties\":false,\"proxyScheme\":null,\"proxyHost\":null,\"proxyPort\":0," + + "\"proxyUser\":null,\"proxyPassword\":null,\"readTimeoutMs\":0,\"maxParallelRequestsCount\":0," + + "\"headers\":{\"Content-Type\":\"application/json\"},\"useRedisQueueForMsgPersistence\":false," + + "\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":true}"; + JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); + TbPair upgrade = node.upgrade(0, configJson); + Assertions.assertTrue(upgrade.getFirst()); + Assertions.assertTrue(JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()).isParseToPlainText()); + } + } diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 8a6ffbf0f7..4a2e869dab 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -54,6 +54,7 @@ "core-js": "^3.29.1", "date-fns": "2.0.0-alpha.27", "dayjs": "1.11.4", + "echarts": "^5.4.3", "flot": "https://github.com/thingsboard/flot.git#0.9-work", "flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master", "font-awesome": "^4.7.0", diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 374775bdd0..0381aae63b 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -1537,7 +1537,7 @@ export class WidgetSubscription implements IWidgetSubscription { } if (this.type === widgetType.latest) { const prevData = currentData.data; - if (!data.data.length) { + if (!data.data.length && !prevData.length) { update = false; } else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) { const prevTs = prevData[0][0]; @@ -1560,6 +1560,8 @@ export class WidgetSubscription implements IWidgetSubscription { } this.notifyDataLoaded(); this.onDataUpdated(detectChanges); + } else if (this.loadingData) { + this.notifyDataLoaded(); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss index 9b0c323a40..16a2f43006 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss @@ -40,6 +40,9 @@ tb-entity-subtype-list { flex: 1; width: 180px; + .mdc-evolution-chip-set__chips { + width: 100%; + } } .mat-mdc-chip { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 15e3fb1952..865a1b8c8b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -319,9 +319,7 @@ export class AlarmTableConfig extends EntityTableConfig if ($event) { $event.stopPropagation(); } - const unacknowledgedAlarms = alarms.filter(alarm => { - return alarm.status === AlarmStatus.CLEARED_UNACK || alarm.status === AlarmStatus.ACTIVE_UNACK; - }) + const unacknowledgedAlarms = alarms.filter(alarm => !alarm.acknowledged); let title = ''; let content = ''; if (!unacknowledgedAlarms.length) { @@ -356,9 +354,7 @@ export class AlarmTableConfig extends EntityTableConfig if ($event) { $event.stopPropagation(); } - const activeAlarms = alarms.filter(alarm => { - return alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK; - }) + const activeAlarms = alarms.filter(alarm => !alarm.cleared); let title = ''; let content = ''; if (!activeAlarms.length) { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 166e359799..a388f69252 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
-
- - - - - {{ column.title | translate}} - - {{ 'event.all-events' | translate}} - - {{ value }} - - - - - - - {{ column.title | translate}} - - - {{ 'event.min-value' | translate }} - - - - - - {{ 'event.has-error' | translate }} - - - - - {{ column.title | translate}} - - - - - - {{ column.title | translate}} - - + +
+ + + + + {{ column.title | translate}} + + {{ 'event.all-events' | translate}} + + {{ value }} + + + + + + + {{ column.title | translate}} + + + {{ 'event.min-value' | translate }} + + + + + + {{ 'event.has-error' | translate }} + + + + + {{ column.title | translate}} + + + + + + {{ column.title | translate}} + + + - -
+
+