diff --git a/application/pom.xml b/application/pom.xml index 8e7c3cd524..98b73133b6 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -354,6 +354,10 @@ com.slack.api slack-api-client + + com.google.oauth-client + google-oauth-client + 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..9ab33665e6 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/b4CowKWO5UUUoWPlxlOME69NBDL67MtQOsl156yYYa33rrrTbUbMmSJTYCLBvuVhe0EWDpYVwT0w5u1VduvPFG96AP6cfjByxBJToVkmysXr0aTwLGUUcdJbRzztSpU5cuXWq0uvbaa83MvqyMn/nTUVHq2GOP1UuBZdxQQgHrxBNPVLhzOFxv95VLL73UdxVSwh5bt24duG3A1xUVog5aJMBy6agwagtYjz/+OByV7FqYG8GhEFgxThvXArVBwRJgTznllFv/NtUFkxqrqI965DgBK8YgNy4AFCy44pBDDinO0dLQMUlFhpBmVqQbT5w4sZyACWAZH+pg1UuIEGVYdEh/BlArWPo2ejTlo48+esIJJ9TnWFoHUgUsE0T1j5IRNqpgCafmB01grV27toCl0winMZbHfZqTRaDiPvFsPINlQ8ww/bIhxhikjEQIiCFy9uzZTlNCzK7Ce61gnXnmmfH8BETjgAnce++9F0PkvffeWypmnsThNhwVLG1omuOPPz7AevHFF+Px8Oqrry5gGXDvueeemCVHmBz+oZC9//77rWC9/vrrpkpr1qwpYHGr+YFbDbDCR27eXfkKH82fP98E30fu3oXeR4wQWGalp59+ulgiRCn5k08+4RyH9EazeCc88MADorsSoo1bwdI5nawEcSgGULHKIcXG0FHeB8HIo6sN57uKrxx88MGg0S6TK0N51DPAAjrO3IICQTbaL0iFn/KAU+6h6RxDnlqWj84XkPMFaf2pbQAHct2gc4amElobJcopPbnV/w6Vx/ah7M837/nmvcvevKclWAlWgpVgJVgJVlqClWAlWAlWgpVgpSVYCVaClWAlWAlWWoKVYCVYCVaClWClJVgJVoKVYCVYCVZ6KsFKsBKscQPWJ5UlWLu9o0YbrP9W1t53y/9c07iFfr+9QuirmnYOXX+2s2BRMI++o9qwej2JYjhk0JoPxWkDgUU8FCITOl1Ky7ECi+6FaOTUU0+lCrdNv9+2B4877rj6HhIU0hRyFAKkzsG66qqrjj766IMOOogSkDyGym2swKK9adoYwOiHi+yCzDAUxdH6/Z5PpnXNNddQZIVQu02w9ttvP7LBAIsMje8oxOm4faQZuuiii8iJzj33XJo1whISXoqipvwqRRZS99fQe7Oq0wq7c7o2mklg0S25or5FBYUM6m+6PDV0txRzNGf2kPD7Lpn5BRdcQG3mctddd50TmsBypv0kWdRswxKxyN1CIM5X9HCK1UihkfebcN4h2QbotEL6N4yO2hFYio3EJKTFEjG4XzozLct12ksUnzZtWqMSe+pjBx54ILDsIZrdZ599MHTLLbfEd4sKyL3wtj0kpe1HLKWg2FWRRIYq1YTGIG7kGp1S0dzHCza0qPPvv//+JtKlbAj5dvGXwDP0NClaiCaxHr0Y7TV00KxAnlIHbqLPNNKR3WGRQ2EnfghFpJHcpIER3wQWIn1F5d3LoOF9p8BSoB6oW1NiwkutdLyoj3hGdnzSSSc1fbdDR9Vtzz33nFjZ3nvvTQcW13J1HqDTd6cHHHCA+sjXQP55xBFH8BXP2H/YYYcByx7nH3nkkY1KDavdCUXrXV2CCSJH9xJS23bA0smkA9h///2B5SZxqiGfe+45YJlgPvjgg5EsANfigewDXCkvSr0QKVBuuukmKIS/aG2dNvSsUbosj8TsSmiJoRANSCIUhpfuRSruo50yXoipTuYagVqoU5+nn37aV0S71qEw7lFSEyXThY4EWCpMUKo3Rq3oew3r9kcKjGF01I4iFlDirgMsM4oCTWSUUCtNCX17KFqbwAKArwtR9fJptTV9iGzbB8sGtCObBadIQaEhC1gcFGAZmxyyv3VYUT4fRQoAkWNnnxVchaiXR6SBqIMlcE6oTO8JsKAjT4RW3HfffXUs46D6uKiOJe7a77dmDsV2dD5BQjkAxd8ogCV+8CEVuJvq9xKdOGpHcyz6ZlfkkwJWoFPAilwjzjHzKWDxlcYV2Pbaay81EVBK+RrCfvPvuK9heCqsq5lbTf12NKDojjwljVF7XRAxrbre1vqYMaDfzKAMNOUEh6JuABIM2niCG8bXDQO4sUNHtddw/T7r/V6ZMCEs6Q8h5286YQCF9Oi9bviuspG+ioFf3B5g7G87v82ovccaHUcN0QxZ4cyd9Vu+ec8377v4C9IEK8FKS7ASrAQrwUqwEqy0BCvBSrASrAQrwUpLsDoDy993t6QNZmUZoy5x1/LlWx5+uHt/Vq/+X0asXTJi+aP5scd278+SJTkUJlgJVoKVYCVYCVaClWAlWAlWgpVgJVhpCVaClWDttmB9W1l9T79ih+Gy+j/tt7HE2ZiANXA9O19athOwLH85xmBRkNXFisXeqKy+J9Z3HAmjWqEpJf3TGHRapGaEjeUoiRWxVOsKiaMPliUkaYsJnRvV8pa2qaPqmSboZkMv5VDcVOy3dOUtlTWqdQZpSp0wqMp+ALCuuIKopKE827NmNSy/SvMbhyZMsMKqFakp5ccUrNsrizWr3TmlIdRo3zgIWCTIjUpl5gS6KzHMyVxGlqm/Ut9rcsLRTprTcrSxejv5kZYjaYztOOrPdmpFUTgKa0sPCpa/39FahnbZ2rj07CRDoZilc3eUc2JVZoI+v8mqiPh0yO2VxXK6pMlkkkTenUQsa/9Ondo444y+baWuWfMPWJMm0cs3KFg16ZiBRYym62i8WDSW/tNvOkwqPFJGYNluVMvR0mdqbHhFIgCOs961Dqq9p0yZ0klzhu6UJpYel/eJJGP18jgqrwRJqhwNIRMd86HQYrsBVnhGT4gA1qjSH2ArwNL33Ehde428AMtq9fYrhNK1PbAoUrdvb+iA0BGf7Fm16h+w4ufJJ/si2ZiBJRpDhAKdeFcDR5wI18RQGIG9DlYkbuBWeuWVK1c2qhwsHbao0YRsMpS4/G5byIxDIJOywUZkxekesFCiW6pqqK5Dw86TRMbSgdCjNqpcN2ViWsCS+wR/9geabYAFJmOfDUPe+ef3A5Yo8frrfUFrzMDCTShohQQ5BQIsgnR+ESeARSeu88GuFazonUaxzude5itIkprCNkW8bZHApEqtxE7XEq5GbobXHliySMQcS74NbJVMMnyii6rz3XffzY2GwpgvFrD4UGd2j01T2J0aCuWFIPZ+++2/6AmwTNgXLuxDTV0cXb68MXFilz0V1vXaA4vTR3TqU7r7UPTjo/9U6N6jhv0+LxfUWo+WbACdPBWeeGK+x8r3WPkeK8FKsBKsBCvBSrASrAQrwUqwEqwEK8FKsBKsBCvBSrDGDqxUQu+KSug5c7ZMnty9P2++mUroXTNizZ7dOOSQ7v1ZvDiHwgQrwUqwEqwEK8FKsBKsBCvBSrASrAQrLcFKsBKs3Qespn/NpmUob6L7teH9b/eBSxsFUWGj+5TQuzBY9KhludQmGYzFHUM72qjWy+zt7V24cKG/M8Qea5FRa1EtU4BZQZOAh/KJvKc9pHydKpWsxVXuqMxioSHiIO90lMJsFJZ9Hx0ldKPShljdlA6sbbAuukgFGvRy8dGZixf/c5TciU7acqVdAZZty00TWoXCPcCiI+VBYFE7UeIXjSUBk0XudFm6P2B1IoZ2Uddq/K1nbFRpI+68887YVrKP27Zts+jtmIM1LEroRrWirMVOOwGrp4do0cKqfdtnndWnhK6DxZ2kx/5O3BVgkcIZ/opfNDaNqIXsS8SilqYmjZM5xRLRIha8SsQKPtow6JAxWvC8BMhyoTCrtGvRbhgKO1dCRxoBzmwbLMRs24bOxsaN1oHu0w9eeum/wDJykEH39jYOPbRrwOKR2EbJpEmTyOcNVa1gUb77TatJ7NthxHJR6xaXiCVnRn1xYiZnhFG4S+ZYnSuh3Z1hwULXg65fvyOwjjqqDxobBKsiuywQ2nDz5sa55/bttIK4nAc2Nmzok7OOGVjmBNwRaRqawNKxOE63C7DmzZsXMmhm8Vzf4ikpHkrEIlxuoznNpQwZBhE93kej7SrC3go4BZrDHXPMMQrvd+3uMQSrbSV0WCcRy48ily7tkztHTDrnnL6Iddhhfcp6e8yuliz5Vwzr9tcNdVV0LFU9XI1aGmaAB9VueypsWwk9LE+FFqJv+2i+x8r3WPmCNMFKsNISrAQrwUqwEqwEKy3BSrASrG4Fq/zxOG0oYHWJu7ocrD7Bqj/GjU62xV3XvPstYHWJu7oZrNNO+7W397s9vPz1DwLfpu3Y+Ke8Iu8Sd/X0fLtwYTf+LFnybW9vn7v2yICUNhL2V8T6Pi1tmCwCfM6x0obZ4ASqfCpMG5GH6HyPlZZgpSVYaQlWgpWWYKUlWGkJVlpagpWWYKUlWGlpCVZagpWWYKWlJVhpCVZagpWWlmClJVhpCdYomYTecgVGJshGlaTPx5Kcsm3z39bKkVA5Pkpx5uPq1aslJ36jxaQmlCg1tuvp3eQNjG/ZXrt2bdPRDk2yPxkuBz5HbZ02OlkwdzewZESeNm2a1LQh2Zs/fz5XasUOi4WUchYtWhQfpS31UfZKDN1dmRyNUn3GtpbbsGHDtZVJchxfoQWQhtmeOXPmNKpU5LYpTxKsXWYoXLp0KfetWbMGZBJ9y2gakGlFoUtLy1MdZ2r+iG2yfPf09MTyBTZk1JVpuJ41eUdglRNkMo70sqVkJ8jxXNKJKzb2DBEs+UKlJVdOEbWKviovj21dqKLOThMg62BxvoSluhMP9AuWYrmiXN3t2yP56kZZkSvbtGmTPZs3b474Wk6wf/yCZXyRtXbGjBlSxvNjpOzmMll0r6tM5AgPTp8+PZLnajNnyqXbqHIMi3k+yqLbBJb491NlRsahgIUqJAXHMrPLlgv0oYCF4FgQICIctiS0VSuluYr9uG9UKeljUYU4M8By6Rsqs1/4VNsmsJQzdepUGxL4iqMKl3G4XE4a/UaVN1oJUqNLf9/vCeN08r5ixYpwxMyZM6PHz5o1y8dtlWndSJW+I7BwuX79eiLJJrCabFCwVEMT+q11HZUQ2u9BwVIZTChczRcsWOA0kSNaWmQSLAEhBbIzBWOUOL8esdyUmYDeJZjZKX43gWWNDyWb4dnWwdxp4GLn3LlzXcV3FRX195V169a1njBOwZJdmPe5QwPHHlSV5QL0P97hph2B5YR+51iSYPdUZkgaCljGDucIVMYdrMBoKHMsQ7BDKKzvlDy8LA4g27ZKGuZQW6oaYCHYhp4zo7JyU61zrFhAxUzARxtIdX44TXRUVMl933pCfc2Vcfe6IaK3/h0feaQs6GBDRx8gYu0IrJ2dY4kW0d1dSJnR6oOCZYbn0LJly+JCAo8IgSqFxAkRvXQMd1F6S4BlMqT+burdv608ye4IrJgwWJOnfIU36mCpiROsLFQ/IcH6CyzLOfGUMMChgofhwE4LEdhpphLjwkiApaVjxuZhIr5VB8sJ0VSiYPmueU8MdgrRwGq7devWqKHzvbMQqGLUUwG1dUeW7ShDocr7ilFMmbYNowOD5QTnC6sup3xOgGwdrDjBDdZPSLB+LCho0esrcyj2c2uZ9vLjSIBl20OAxwW4tIJVrCzAEWY1CvMk+30RAfFE4ivx8CFKRajzDIG/WB7MoiYBlmeF2bNnu83YX3+E7BeseAgw1MasNI7WwYq3bk0n5Jv3f5nWrS910agSUzU9k3ePAbFpgUIfW2tbf+6rzzJLp2r7cjt7wvgFK22XtgQrLcFKS7DSEqwEKy3BSkuw0hKsBCstwUpLsNISrLS0BCstwUpLsNLSEqy0BCstwUpLS7DSEqy08QiW/4/2r/65LmjacBmcQPX/9jjCDkWUOTQAAAAASUVORK5CYII=", - "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\",\"displayTimewindow\":true}" } }, { @@ -139,7 +143,9 @@ "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":true,\"entityNameColumnTitle\":\"\",\"displayEntityLabel\":false,\"displayEntityType\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"useCellContentFunction\":false,\"defaultColumnVisibility\":\"visible\",\"columnSelectionToDisplay\":\"enabled\"},\"_hash\":0.6401141393938932,\"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;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}]}" + "hasBasicMode": true, + "basicModeDirective": "tb-entities-table-basic-config", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"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;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" } }, { 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,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAfRklEQVR42u2deXwb1bXH89+DAqV0eV3f62uhe6Hs+9ayFRpogEcgoSwhbGFrgZbkASVhJ5BASCAhxImzmiR27MROvMZxLHmRJdvyvsi7LW+yHduSRttImveTrqzI0kgajWZkm9zzueRjbI3mzp3vnHPuOefemcdRoSKDzMN/Lperp6cHPzAMU1dXZ7Va6bjMLVGpVKWlpfihsLAwJyentrZ2VoB18ODB9vZ2/LBv376BgYEDBw7QWzWHxGQy7dq1q7Ky0u12p6am4jf79++febCMRuP69evB1tDQEEGKgjVXxG6349+urq7s7Ozq6uqqqipy79LT0/Gvw+GALZoxsEZGRgoKCiYnJ6FCCelpaWn0ns1CASVQToODg71TMjw8jN9DNYAkrVZbU1OTkpIyOjoKNYHfm81mvV5PPtnX1zc2NgbUEgcW9Ge6V7q7u4uKinJzc4m1xmWQPul0ul4qXsF9SjxPcHkBE5xgwHHixAleOPAn+FW4lfiAWq222Wy8UMLPId8zMTGBD8vuY0UWDCjVFokXlmUNBgOedrgoxORJqPkAFgjDcwKtRsE6VQQqimhHTNJlPZHT6YQjBBcNVlJyBUbBmkVisVjAEwwfbnmC55VQYFCQEjr7FKxZIfCccGvhjM/gPA54QXuNj49TsL4OAhsEVQGq4FSJoYGxDIyMtfX2mxhPWNvl5kz2uIwazCLwij9ITsGaYduHuwhVIdjvdvcOGY6pa7YcyH7zi13PvLv+0X9/RFq9rgsfMDDun200nbfJdPE2852pzAv51nVqe2E3e8IaA20wxLDIJJZBwZp7At2AsRVi+xwsW9mkS0rPef6Dz/0kBbVAsHjbzSnMe2W2in6nUxhjCI8BetGhLwrWDAhUAkYVMafogz9o2HX46LPvbQjHk3Cw/O3y7ea3SmytY9GBhnVGvEO4QqVgzbCfDk0QGsMMkpau3jU7UqPyJAIs0n6+0bT0iLVq0BnVBezv74dypWDNagFPoCqynw4v6sPkfcKREgeWvy0+ZGkciaK9ML2I1eU6ZcBymrihnSLbiDRZeQQ8YVkiOFUWm31HZv5jK9fESlU8YKGdu8m0vMgW2cGH4YbqomCFiLWbK/4Pka3iFxKc32pFTCFCgLuxvfvlNZtFIBU/WKRdmmzO7YikSlGpIJwtClYiwEKyL4KuQhAhNV+x5I2PRFMlCVikQXXZnJH0lkCbSMGSHSx4VJ2dneGyNIhwivCo5AMLDQGwPmNYe430IipzKFgzDBZsX4Ro0Mj4xIp1SfFTJS1YaJclmxvCe/QwiFFjEBQsecHCPQhXmqIfHnnxo02SUCU5WGjnJ5nL9M5wTwsse+S5LQVLRrDg7YbzSIZGT/x99UapqJIDLLTffmlSDzjDRePIAhwKVqLBwtDjseb909iEMZ4JYMLAInqrzuCK9bGhYMkIVriCBbuDXbVpp7RUyQcW8bcGTPxREuSqw6UQKFiygIUMLmZPPN4Jx23clyU5VbKCReaJVj6HClPdcFqZgiU9WGQmyBsLLSivkoMqucFCW1HEr5kQekAFPQUrEWBh+QNvuXq/YfTJtz6Zo2ChFXTxTwN5nyIKlsRg+fcrCFVjKM2TiarEgIWcD28+kdfuU7AkBgsTJV51VaSukY+qxICF9noxv0GEpxWktKKDVduufyTLIq69XGg9pcAKp67MFutz73/2NQALRc/1fNEHhB6CarZ8YBUXF2Pe2NbWlpeXFzQ06la96H5cu8t8SoEFi8Cb60grUMhKVcLAQns4yxLO0woGCynStWvXYkSw2wwXslcJBUs4WLxzb8ZqC1z1MNfBQqvkqztFMWCgDzAPQbysrCwoKoBFdikh/1KwYgULKh/zwdDfHzxWKjdVCQbrsSNW3phW4N4W8zo6OrA5yYYNG7CZBNVY8YCFLTdCN1mA1yVhpnmWgIV6+e4JF2+ywe/C+3ysxsZGPHDYWAaqC5aRC9htpqi6VXQPrkyemCUbxQx0losGiy39mZDdZnjtYHVzWwKoSjBYaB+qePYpwSpqf7A0+qyQaiwhGguVx7wZ2Z7B4R1ZBdIWMswsWOd9YXoo05KpY3knxdgjiYIlJVhwXbGsOUKSp7W7b0924UtrvpijYP1qs8e1Sm12RF5z4Q8pULCkAYu/OMlp5Nix0MQO3PkVn26dE2D9erPp8WxreitrFLYlBBxNUi5LwZIGLP78xOB2z+GaCz1ryBwjvIS9un7bLAQLJX6EJ7Mjti1GkN4hkVIKlgRghQs0cM2PTv+e33ADSZwj2BUznJjIL696NyklnoU6koD1h63mZ/M8PDEOkVvW+N0sCpYEYCGhwVs6wlX8kv8Ly3/K9a3jbH3Bgftx8YTFA9ZFW81IvmFTGoeLv1gjpv3+iFdAwZIALMwHeQopnZNc8WlRvrn0B1zPe5y101sCeFJQuwzCPty+/7GVa+UD67a9THqrY9DsZl1R8lQxbVpJvII5Apbb5TYZRTbGLDdYGEqeZ3pSFcMpSr/P9XzAWdqCCMOqw9KaxnW705euWisVWPceYPI7WZNgY4fZLua8wu8VTCEM4twAyz04YL7pcnGNWXxXAsDiMyG7xJxL+S2uYwXHNMJdCaqPIIQ9vupjcWAtzLDkgqfY9/vDM+OPTgkREnmhYMkGVu9q8Wf0EHYW1/YCZ6rl3M6glLa6oQU7+j319johYD2ZYy3pc1rYaTyxLmf1sPYz7ca28Xbh1k2gYBk+8s4ULNnAav9nXGCdbGdwrU9xxgrOPS3YjdU+NS3tIGzZO5+GgvVMnhVLAu3TqxAcLod6UPNJ1fpFR/42P2MBWnZnruRgYR6D2QwFSzawWpZKBJa/nc61PM5NgjBHEGFVTW0DBs9+CggTYBlgkDNuc9pK+8s+1KxdmLWY8ORvm+u2SA4W1BWUFgVLNrCaH5EarIDWeD83oeBckQp0LayluE/xXsXqezPvD+LJ39ZVbZAcLEwhsXSHgiUbWE0PygiWv9X9hRvL4ZwnwwF2p/1oz7G3yt+9J/O+cDz5G9SY5GAhHw//nYI1NzVWUPNEwqbGinMvV7wWFSnSPtNukhwsbIKCrA4FSzawdM8liKr+jb6IrLKI88Zpraz13qyFQsD6qmWf5GAhXYjCLAqWbGB1vpoIqmpvIzFVV1e7+Y7r7OtW++zwWLMQsBR6peRggSqwRcGSDSz9BtmpUp7j2bTXM/GzWpYsJNfLFuWT8yc37IgK1ph1THKwYAdhDSlYEoDFX4w1elh2sIwaX0Bh7bsnr/evN7mHBj06zO16pvD5CFQtO/q8kMHHchtUWQm/WXh7GWr/KVgSgIVt+3i2GGWa5KWq83XfjVccC7pk6z+e5Lwb6U7YJu7KuCccWOltBwX6TEJeojEnk9CzHCyEbXj2g0QqpuTbclGluQCJec9JDEPMgptDr9qxwxf5RGiUl6p7MhdO2ieFDD4em5jeqDOXymZmOVhkgs3zB+2N8oB1uq9a0Om0vvAE/4XffKVTW0l68aF6bShYe5q/igkUCtYMgBW0VvOkdCyXBSx4byQWmrw50oU/MN89OeFNObMPZS8JpGpJ7hNWp6BtNaCrYnohBbwr+FgULJkXU5w4Kj1ViLsSmuu05luujHzttpXLfTNUk95P1YJD9zaONgkcedQwRlh9xDslJLtXULCkAQsai8d/d9m4ku9ISVX5Tzi3Z6Wo22hkFt0l5PLZLN+GCZnth0HVnRl3C6xoIBJuJ8ioNY/zwONXXoEGO378eH5+fllZGQUrVrAmvMLzh8ZFUoJl6fDl4974l9ARuP1aV0cbSfWsUL52qD0rpjoF3p1UhWjueaTIoaWlBavsya4NqampFKxYwQrrZo1mSkaVfr3P7zmYGtMIWJY+QFI9TrczpmHHzkQxvfw8cLXSPDKpwb4g0GAHDnjenzYLd5uZ/WBx07fECOi6gyv7sRSpm1unUjcdSN3EOgj29R/GOubQODGVupPQqH9RyTy4/Tt37iRvR8FuM/gughfGaNIrJfWdosG6eodxUgoxtulEg2V6YD6+wTTSIPqmusrP9XXDaIwwrFD8+Azfg78q7tTNt3ypG7vd8sRicePgLDkew5PsdmNvmJhWfQU5ZB4fq8orcLugurRaLeppyFcTvyFOsCakkEldazxg4RuMhvp4wPJ1YzJSRJHsz8M3ZTdwyrPjAgtVo8TWfPK+eM294Cb38KDwyWCsr4IGPIEOGZ0VSrmjH1az8L8+Tves+LN3+EIGrPKY6EEISvVEFuiayBs2hZsXB76Jg4IlJVioyuV/vYx9UGR6R/OHqdTNMHP3LXGC5Un17EqKfAnQu7H67CRRHaStKVgSb8cd9k2qYjytqdSN22V9eVn8VHnarVc562siuFaYgoR7PU4EQfkD8aAoWHKBFTb242I49W9jTN1k+vTdji+locqX6rnTbZwMZ85ida1IqCXUuaRgSf/Kk9Dd9KdqK4ui7+bgb02LhaduYm22Vct5qxj4Y7zRBLGr0LQPBUt6sDA/4t/VCILFzYJSN//lS92YjOi/tFT5Uj1HMgItIKYdkee8EbLUvHNhCpYsr5XDfQrdQdmXPay6SkDqps0XX3j7NTmo8qV6Otv9vY1pP5mg5CBvtVYiwBpmDHcfuk9ce6rgmbkIFmZJYcuYkO8r/WHE1M1nPmWQmSYXVb5UzyKS6ok1ECokOp8YsIYFLnMLbY/lPTEXweK820qFdVmMas86CN5zoTaQpG66O813XC8rWJ5Uz4Y1os0ICUyEg5KCJePLxqG0+A2ih7sDnOIbwSdSnOnfqtQ9YrC9uUJWqlAJiHp50WDBCEYo1aJgyQgW5uERnmnOkOohKfBEvauDv0Gjsix7WHqk7rzRvn2zO5YKvtDEKCr9I3yAgiUjWCQWH2nt1Eg6p/zmVP3Cn4M2W/PP2ZylxdaXnpYGqftu9yA1MR7PXAoR1KgrDSlY8oLFeXe4419qQQQ7SiK4gNIae5S1eyiYsSd9zvztbjHjcMd1iF2xxYUc6+DiE8xLUPgQNedDwZIdLJLxwMLzsH+2dnHjxTF4zb3djkOptg9Wekpobrsm7IUvvMO64u/2bZucVRVc7FmacMYdVAlZDUbBSgRYJFYkIlsiqHJqbMTV3ooAPQBCc7U2ufR9XLhJQ3ynQlJBYCaRgpUgsEh+l2dd6xwR2D5QJTyOSsFKEFiELWR5xWVOZlbI9DamdWAUrMSBRQSF4ZEn6rNN4FGBKnuMtpWClWiwyDwR7rzoREoiBX4hqAosDaVgzV6wSHxLhBpIsOABgO0W9wBQsGYGLOK4wB0WVwKVAPOHvsXjDlKwZgwsIvC3IqUUEy7QT1BU4YphKFhzBiwSy0aUK9a3t8khiIbAQEdePknBmjNg+W8qrA+0RawrZKQ6OxQnlhhJBTcFa7aA5Z+FAS/cYP71iXKeUVqgKVizCyz/nLHXK3CfZbKPyMwgogak4OTJcQoK1mwEy59FQdkT7j08MOSw49dhAAjIwpnDdyKQZpMoM03BmmNgBQYmEJVASAluEP4le+kKCVqCJKwjBZSAqccrKJiWlafoYJEtLiBV9c0blMPi2tbyIXyDrku3p+orcS1Nm+7pRGurYUeSuDa0bze+oK+76UTjanFttGkDGQoROxrIARnZSxfLAAFKb0SBqoPzBHuaGJhi01hUqFCwqFCwqFCwqFChYFGhYFGhYFGhQsGiMjvAOnToEIpvioqKNBoNXktRUlKCTZQLCwvp6FCJC6yamhqAVRkidHSoxAVWQ0NDoMYqLS2Fxjp27OQ+JPPmibSYMb2RjB74tTkQnxdEDAWLHkjBogdSsOiBFCw6dsIPPGF1J9U43BQsCpZUBwKm1GbHpcnmK3aYP6u0U7AoWBIc2DXhWnzIcstXTEqjI7+TvWy7ubjXScGKAla/KMGLW0+FA7v6Bt4uHL1gi/HFnLHDdUPZ3rZOOXJhkrGybeAUHBx8nmqseA88WDN8/W7mwUxLbierHnAGtteLrQvSLA7XDHcVHWgYccFGL88b/Vhtd7oTq7Gam5sRFFWpVPX19UEpHQoWr4xY3C/kW69INm7WOoKQIq1iwLnooOUNhS3BXcXsoaTPuaXG8eJR6217mV9/afrTHmbJYctL2WML0phHD1vNDnfiwEKJfrNXgBR55yoFK5zgthxoYeGkP5dnzdAO8VJFmqKXvWEXk9bCytrVIbP7aBe7Tm1/7LD1ul3M+VvM8/dbns61rlLadtY7lH2+zsBAq/qdz+Za4Qjqje4EgYWX9mJlDsBCbodqrAjSPOq6O83yl/0WsEXuVgSw0EDVRdvMrWMuabvaO+l69bjtrlTLb7eYr9nJPJRp+b/j1o3V9iPtbLie+Lv6Zon9iu1m7ZAzEWBFcLAoWEQYh/v9MvvF28wflNsqQu5WhLa2wn7jHsZkd0vV1TqD6/Lt5uXHbLsbHFCKUTsQ2tXN1Z4LyWpjZwwsqrGIHOtmr/UqBhidcHcrQluWa338iNUtRVcztMOwwl9U2wXyFK6rac3sVTuY1eV2NwVrRg4cZtwvFlhhbhBPj3q3wrVyvRPWc3O1I86u7m92XJRk3NXgiJUq3q4WdrPzUy3P5lmtLAVL/gMxIYdLhPn5vxW2v6Zafr/FvLzIVqoXerfC3tcOj78P91lcV6FXPlLZr9vN7FQPi6AqXFcxf3w4y3LPAcuYxU3BkvhAjGiFbjBTx75darsvw/K7LWaEppYesbxV4plVRXZihIOFBp0H3wgqMNauIhb1j6NW6DzomJjOKKSr8BdfOWa7fhejG3PFP6oW9tQGq9/kzu1gP1TZHzxkuWCr+fLkSfhPiDkl1zqO97Dx361w7dXjVrDLumLoqtHuRjxscaalpE/MGQV29RON/ZJtIWkot1PgqCI2dlDHPnbEetFWc3vP1xQsTN8MjLt7wlVvcJXpnQVd7CEdixTepmo7SMLFwyShIbX32nEbwpvwx2W6W6ENpnBhhgWqUeA1Dpjct37FIBKLA8WdUXhX9zQ4Lks2Q0n7zm3t4bQ3cCXfc6gu9bz8vPttzpDGMY2BbymDc5bTwS7Ls56f5BlP0AkFL6PGwh1F1E5usNZp7M/kWZFOgSd0Swpz9Q7z+VuMP99oQmgHDx8mcYg733OAwQUvOWJ5Jtf6r2O2fxfbPq+yQ1fFyUc8Bxb1sJgK7NYYol5g06jrqh3mt0rsCevq4TbHH1MYRFZdhnSu9Af8+zopznJrLhyuXJR7bOU/9+15KbNqTYWlKEDNywVWz6Tr0m0I/jJQifKBtVlrBzfrNfattQ48auktLGKDadXDFYniI9yBTZ1l5oqrWzqORjhqbxOLFHX7eKTdGRExx+OB8psEPwOK7snC/KU+hhoX4eVkI+3p3MAWru1Fd93t9pL/CUXNrfgmU37JWNWD+vq32nSpA/qOk8Tk5+fjjWEKhQLrKbCqIijyLjyz3dQ1cP3OyVfzx5ZkjC/cN9qrlyWdfkBruHTbZKqmNbtuMHuqoAAtTdUZ+L/Cm1QHKrQlDuX3yXAPlS8oqqkMd+Br2Z6B0nXzX+BW1QjCChuUI/J1lbcptUWmkt+h847jZ3+csYFUZzQ1t+TUDb2SO3bJVuP1OyZXZHflqHLqNJ92VjxvKLvNojyXKz4tgLPTdM3VJ8Gq8IomQERoLFhcTFxhcUjk5o6vJt4ptUmusVoM4ytTkwbL/+JWfINRXaZry5oRixZ6YE1vj730PAwuNJZL+R384FJ8q79+VWW/kffAJ3Osz+dbQy9wfaX92l3MQZ1DbuUa1LqbNroUZ6PblvIL67vq3i2zIfOzUmHDzAZ+Hn6AoeQ9sFI/0dip7mzZOVD36oj2iWmmUO0Vv8YSsfwL4R+MFGbpMEbavkGcDIbpht3MnkaHNGC5rNxIhq3uAfvxs4NUsaniupb2YzMLVrV+xKK6yNMZ9Y2V/SZtX99o9SPkUbaXntuu2xt6IGJjsObJdSfHx+niEDP7816moItNgNX2N3T+RNV9ZDANNcvQ/6n4CIv5zTaVIaYTSedj4bWzJwpzFZ/k5T05ofmzo/S/vXb3G6aS3/dUPr0ydWtpR2ccYLm4iVKu/SWu7Ec+i158uqnihp6mT2t6e7uav3SU/nQKrxtU1ZkzAhbuhFH9R/KsV+tP3obGzgqT+lrSPaP6Tw3d2qAD4RdifoofyGR2yRHr/6YzEkbOhBzY2Kmylf0KPXQqv9femhL/GUWB5WI4UzU3vJfretMzBa285OTrsgOas+Q/3YozAn9jL/8d1/I4N7idY1qEgeX2vC+57UXPK5OnvqTn+FUp+WugDKbr4cm+hvdYpW8KM155T0N3TSLB0vTbxyvvJZoJ1jDkM46uliRHyU/IwzZc+1x1nyHwjNtqHVduN2MePT+VgcpX6aOcsaC2FfpvRLtkuPaF1racyn6z6GtEz/UN75A7hQegtqdDksGJBaz2f3J187mKX05303zNUnJeRd5t3dX/6G7aBJOk7e0n97uiOh3ThEnN7fbi7047pOwnXMN9XN86zqjm3CGG0txgqn+Jq/jVyc9rLuC639mubkKZUbjUSpV+tL/+Dafi295DTh+tfqiupzkxYBlqnsZJWeUP67vrw30M3RuqfdmtOJN8srvp85y6fv9fVxTZzttkghMT4UT13Q29jWuMmpvdxdOeWJfy23iWoLn5mI50jdD3k5pbyHAN1L2m6bdJNTixgOVXP4qzOM2FXOMDXNcb3HAKZ6pS901gVkyKk8J1CE/GhuOqpCOfOBsXc+U/nQaZ8hyu9haP/hvL4XpXe1Sg/0+qc7mOFZxJiz6gwOPqnZE8D9KO1jbi/hEPFA+iQftkTW+XrGC1VbziddLPaeosjfphkIfHzGcZSy5sbi/yR02ht0I/r+lnWtvzhmpftJX9+uTcvvjMSc2tvY0f9ze8aVZdGfioYyqDp6upoxxqMvI1YsbDlnhcC/gtLe0F0g5OLGAhjDGayVnaOPe0PLjuhOuSZHNyLRu1Q/Do4dcjnulCoszSwQ3t5nTLOM0feFRg2Q/N2iXchNIf4YWZQGAsrYUVyAeeXXigRD0gxIIbQ5So5GBBQxOCA2em0VnUpftBGateFIq+tlcP6wlvmkwtSYMxHdE+1t66P79WF/LhrUEfZkt+PKp9tL11HzRl0DUCVjx7ZNgnKudjmiX5Uxev847Y+jW7zOs0DoEdKut3Ig6+pmL6gjvHKDea5dFMyB40P8KNZcM4BjrvOMvVO81faB2xKp7antbR6oeh54m9gLYPdKvjB6tNlwafCXeos2VbrMfCMWpVv4ZeEW2nr38TngM8/RAldBqjury/fiX8a78SCtdVENPSnu9Rb+W/CQhdngHrCRsKS4oD63paGNUV3t+f2dv4kXqAlUOdxwUWko537GdWKayxFgAhi54SLQDhBwuBsbvSmHdK7KJdJcRjvBPp08iUQt/wdpV+PP6xgxUjBrdV/bpo5wyadazqb6RvRL/63CbFOVNuU6+4rtZ1N/Y2roULFfi1jPIXTqXH2YW+9JIqlwMqHiwk51HEgwIxER1C3A8+WWlf9MWcMJpP5VgxUYp/CDBPhNHxzViV30McD5Eb0WMHWMkkFFO8+Cf/uMfmims8k8qyX8CCw9ghBCjVbYYi1LVl4msdU6mYE1X3B9rHWQQWfCTE1lHI4c+3x9oh5M+xULgjfKaMgPVBuR3Z5bJ+yYaguaMYwaQpvL6PmLjfOAofO8zJyU0aq14M8yRJVAmTG+FzWLFndJRpc9vaMhIQ5BMJFpyk2/cxQmr4I3To4wobiunCVS2iZ3DVkdY42sVKPgRNHcrxygVTc/XveF17vcCxq+4btpZfgANhZeDTzHgSaXYeOA2s8vJyZHLwLzb4i7D8a2+TQ/j9jtyhfxV6yjXtfCYRKc+Lk80ZLax8Q4AaBC9epxHXvrvi6aC4K5/HbUTuyBO3U13styYUrChg4U2bQKqpqQlZQoAVuAepv7oBNQUXbzXuqBiWJJ1+pG5o8YHxxw+dCCqAQEb9kq0TnypHE1CkoNQeGyy/x4eX4qxe1ZJjNTW8n8yp60Mmn7jAhTX1s6eeYhYeOG3vBuydjLKZCAtWEUyCFklpckhIOmptEUxfH7DjDyabt+5lXs8fTeRDWd9Vq1ct8sYOEH48CxGg2m7d9M+wI9ql3vjQj+p6mk4RxZMIHwvrbrHnE2rrJO8QAhCwrRmtLKmPWHrEirVWMzJ2CIsDKR9eijPxs9+hHqx7xevvf7exS3Pq8JEIsG5KYbD8V6YOZeg8m5VVDTqRLLs/g0EWdgbHDoFElLtM4XUGgt1IkhBNhvDjKcVHIsD6VG2XtUPbahwXJJn/lMIU97CzYexqe9r9SSESAUcV26nGRyLASkCHkBo60uaYVWNX09OJYAQi7Mj4noJ8fE3AmrUHInZ1avJBwaIHUrDogRQsChY9kIJFD5wLYGV7hYJFD5QYLIVXKFj0QOk1Vk5ODgWLHii7j0WFSmwifHWy6F2yRAs949w9I32LPZWZBitoqpgAwc5KgXuTyC2HDx+emJjAixRQl5aYM5aUlKBqFyWWgR6trILTodgO1xg4OZthsIKmigmQLK8k7HQocgRYBw8eLCgoSMwZ8ZoZ8uaiwE2jZBWDwaBUKlmWlfuMsWmshD1YRPK8krDT6XS6yclJjPvRo0cTc8Z9+/ahFrympmb//v2JOSO2QEPROZ4cAC3rif4fXnSErX5aQ1IAAAAASUVORK5CYII=", - "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/application/src/main/data/upgrade/3.5.0/schema_update.sql b/application/src/main/data/upgrade/3.5.0/schema_update.sql new file mode 100644 index 0000000000..321112c4eb --- /dev/null +++ b/application/src/main/data/upgrade/3.5.0/schema_update.sql @@ -0,0 +1,23 @@ +-- +-- 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. +-- + +-- FIX DASHBOARD TEMPLATES AFTER ANGULAR MIGRATION TO VER.15 + +UPDATE dashboard SET configuration = REPLACE(configuration, 'mat-button mat-icon-button', 'mat-icon-button') + WHERE configuration like '%mat-button mat-icon-button%'; + +UPDATE widget_type SET descriptor = REPLACE(descriptor, 'mat-button mat-icon-button', 'mat-icon-button') + WHERE descriptor like '%mat-button mat-icon-button%'; diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql index 3bc2c99168..58031ce5c0 100644 --- a/application/src/main/data/upgrade/3.5.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -52,3 +52,10 @@ $$ $$; -- NOTIFICATION CONFIGS VERSION CONTROL END + +ALTER TABLE resource + ADD COLUMN IF NOT EXISTS etag varchar; + +UPDATE resource + SET etag = encode(sha256(decode(resource.data, 'base64')),'hex') WHERE resource.data is not null; + diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index e0190bb388..e04c7052db 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -90,7 +90,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.discovery.DiscoveryService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.queue.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.component.ComponentDiscoveryService; diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java index d5833e0c07..2779a83979 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java @@ -59,10 +59,10 @@ public class DeviceActor extends ContextAwareActor { protected boolean doProcess(TbActorMsg msg) { switch (msg.getMsgType()) { case TRANSPORT_TO_DEVICE_ACTOR_MSG: - processor.process(ctx, (TransportToDeviceActorMsgWrapper) msg); + processor.process((TransportToDeviceActorMsgWrapper) msg); break; case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: - processor.processAttributesUpdate(ctx, (DeviceAttributesEventNotificationMsg) msg); + processor.processAttributesUpdate((DeviceAttributesEventNotificationMsg) msg); break; case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: processor.processCredentialsUpdate(msg); @@ -74,10 +74,10 @@ public class DeviceActor extends ContextAwareActor { processor.processRpcRequest(ctx, (ToDeviceRpcRequestActorMsg) msg); break; case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: - processor.processRpcResponsesFromEdge(ctx, (FromDeviceRpcResponseActorMsg) msg); + processor.processRpcResponsesFromEdge((FromDeviceRpcResponseActorMsg) msg); break; case DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG: - processor.processServerSideRpcTimeout(ctx, (DeviceActorServerSideRpcTimeoutMsg) msg); + processor.processServerSideRpcTimeout((DeviceActorServerSideRpcTimeoutMsg) msg); break; case SESSION_TIMEOUT_MSG: processor.checkSessionsTimeout(); @@ -86,7 +86,7 @@ public class DeviceActor extends ContextAwareActor { processor.processEdgeUpdate((DeviceEdgeUpdateMsg) msg); break; case REMOVE_RPC_TO_DEVICE_ACTOR_MSG: - processor.processRemoveRpc(ctx, (RemoveRpcActorMsg) msg); + processor.processRemoveRpc((RemoveRpcActorMsg) msg); break; default: return false; diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 934f309480..0ce0ae7884 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -83,7 +83,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProt import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseStatusMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; @@ -182,13 +181,15 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso void processRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg) { ToDeviceRpcRequest request = msg.getMsg(); + UUID rpcId = request.getId(); + log.debug("[{}][{}] Received RPC request to process ...", deviceId, rpcId); ToDeviceRpcRequestMsg rpcRequest = creteToDeviceRpcRequestMsg(request); long timeout = request.getExpirationTime() - System.currentTimeMillis(); boolean persisted = request.isPersisted(); if (timeout <= 0) { - log.debug("[{}][{}] Ignoring message due to exp time reached, {}", deviceId, request.getId(), request.getExpirationTime()); + log.debug("[{}][{}] Ignoring message due to exp time reached, {}", deviceId, rpcId, request.getExpirationTime()); if (persisted) { createRpc(request, RpcStatus.EXPIRED); } @@ -198,21 +199,23 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } boolean sent = false; + int requestId = rpcRequest.getRequestId(); if (systemContext.isEdgesEnabled() && edgeId != null) { - log.debug("[{}][{}] device is related to edge [{}]. Saving RPC request to edge queue", tenantId, deviceId, edgeId.getId()); + log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId); try { - saveRpcRequestToEdgeQueue(request, rpcRequest.getRequestId()).get(); + saveRpcRequestToEdgeQueue(request, requestId).get(); sent = true; } catch (InterruptedException | ExecutionException e) { - log.error("[{}][{}][{}] Failed to save rpc request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e); + log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e); } } else if (isSendNewRpcAvailable()) { sent = rpcSubscriptions.size() > 0; Set syncSessionSet = new HashSet<>(); - rpcSubscriptions.forEach((key, value) -> { - sendToTransport(rpcRequest, key, value.getNodeId()); - if (SessionType.SYNC == value.getType()) { - syncSessionSet.add(key); + rpcSubscriptions.forEach((sessionId, sessionInfo) -> { + log.debug("[{}][{}][{}][{}] send RPC request to transport ...", deviceId, sessionId, rpcId, requestId); + sendToTransport(rpcRequest, sessionId, sessionInfo.getNodeId()); + if (SessionType.SYNC == sessionInfo.getType()) { + syncSessionSet.add(sessionId); } }); log.trace("Rpc syncSessionSet [{}] subscription after sent [{}]", syncSessionSet, rpcSubscriptions); @@ -221,20 +224,20 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso if (persisted) { ObjectNode response = JacksonUtil.newObjectNode(); - response.put("rpcId", request.getId().toString()); + response.put("rpcId", rpcId.toString()); systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), JacksonUtil.toString(response), null)); } if (!persisted && request.isOneway() && sent) { - log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId()); + log.debug("[{}] RPC command response sent [{}][{}]!", deviceId, rpcId, requestId); systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null)); } else { registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout); } if (sent) { - log.debug("[{}] RPC request {} is sent!", deviceId, request.getId()); + log.debug("[{}][{}][{}] RPC request is sent!", deviceId, rpcId, requestId); } else { - log.debug("[{}] RPC request {} is NOT sent!", deviceId, request.getId()); + log.debug("[{}][{}][{}] RPC request is NOT sent!", deviceId, rpcId, requestId); } } @@ -242,7 +245,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso return !rpcSequential || toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty(); } - private Rpc createRpc(ToDeviceRpcRequest request, RpcStatus status) { + private void createRpc(ToDeviceRpcRequest request, RpcStatus status) { Rpc rpc = new Rpc(new RpcId(request.getId())); rpc.setCreatedTime(System.currentTimeMillis()); rpc.setTenantId(tenantId); @@ -251,7 +254,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso rpc.setRequest(JacksonUtil.valueToTree(request)); rpc.setStatus(status); rpc.setAdditionalInfo(JacksonUtil.toJsonNode(request.getAdditionalInfo())); - return systemContext.getTbRpcService().save(tenantId, rpc); + systemContext.getTbRpcService().save(tenantId, rpc); } private ToDeviceRpcRequestMsg creteToDeviceRpcRequestMsg(ToDeviceRpcRequest request) { @@ -268,82 +271,92 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso .build(); } - void processRpcResponsesFromEdge(TbActorCtx context, FromDeviceRpcResponseActorMsg responseMsg) { - log.debug("[{}] Processing rpc command response from edge session", deviceId); + void processRpcResponsesFromEdge(FromDeviceRpcResponseActorMsg responseMsg) { + log.debug("[{}] Processing RPC command response from edge session", deviceId); ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId()); boolean success = requestMd != null; if (success) { systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(responseMsg.getMsg()); } else { - log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId()); + log.debug("[{}] RPC command response [{}] is stale!", deviceId, responseMsg.getRequestId()); } } - void processRemoveRpc(TbActorCtx context, RemoveRpcActorMsg msg) { - log.debug("[{}] Processing remove rpc command", msg.getRequestId()); + void processRemoveRpc(RemoveRpcActorMsg msg) { + UUID requestId = msg.getRequestId(); + log.debug("[{}][{}] Received remove RPC request ...", deviceId, requestId); Map.Entry entry = null; for (Map.Entry e : toDeviceRpcPendingMap.entrySet()) { - if (e.getValue().getMsg().getMsg().getId().equals(msg.getRequestId())) { + if (e.getValue().getMsg().getMsg().getId().equals(requestId)) { entry = e; break; } } if (entry != null) { + Integer key = entry.getKey(); if (entry.getValue().isDelivered()) { - toDeviceRpcPendingMap.remove(entry.getKey()); + toDeviceRpcPendingMap.remove(key); } else { Optional> firstRpc = getFirstRpc(); - if (firstRpc.isPresent() && entry.getKey().equals(firstRpc.get().getKey())) { - toDeviceRpcPendingMap.remove(entry.getKey()); - sendNextPendingRequest(context); + if (firstRpc.isPresent() && key.equals(firstRpc.get().getKey())) { + toDeviceRpcPendingMap.remove(key); + log.debug("[{}][{}][{}] Removed pending RPC! Going to send next pending request ...", deviceId, requestId, key); + sendNextPendingRequest(); } else { - toDeviceRpcPendingMap.remove(entry.getKey()); + toDeviceRpcPendingMap.remove(key); } } } } private void registerPendingRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) { - toDeviceRpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent)); - DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(rpcRequest.getRequestId(), timeout); + int requestId = rpcRequest.getRequestId(); + UUID rpcId = new UUID(rpcRequest.getRequestIdMSB(), rpcRequest.getRequestIdLSB()); + log.debug("[{}][{}][{}] Registering pending RPC request...", deviceId, rpcId, requestId); + toDeviceRpcPendingMap.put(requestId, new ToDeviceRpcRequestMetadata(msg, sent)); + DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(requestId, timeout); scheduleMsgWithDelay(context, timeoutMsg, timeoutMsg.getTimeout()); } - void processServerSideRpcTimeout(TbActorCtx context, DeviceActorServerSideRpcTimeoutMsg msg) { - ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(msg.getId()); + void processServerSideRpcTimeout(DeviceActorServerSideRpcTimeoutMsg msg) { + Integer requestId = msg.getId(); + ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(requestId); if (requestMd != null) { - log.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId()); - if (requestMd.getMsg().getMsg().isPersisted()) { - systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.EXPIRED, null); + ToDeviceRpcRequest toDeviceRpcRequest = requestMd.getMsg().getMsg(); + UUID rpcId = toDeviceRpcRequest.getId(); + log.debug("[{}][{}][{}] RPC request timeout detected!", deviceId, rpcId, requestId); + if (toDeviceRpcRequest.isPersisted()) { + systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), RpcStatus.EXPIRED, null); } - systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(), + systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION)); if (!requestMd.isDelivered()) { - sendNextPendingRequest(context); + log.debug("[{}][{}][{}] Pending RPC timeout detected! Going to send next pending request ...", deviceId, rpcId, requestId); + sendNextPendingRequest(); } } } - private void sendPendingRequests(TbActorCtx context, UUID sessionId, String nodeId) { + private void sendPendingRequests(UUID sessionId, String nodeId) { SessionType sessionType = getSessionType(sessionId); if (!toDeviceRpcPendingMap.isEmpty()) { - log.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, toDeviceRpcPendingMap.size(), sessionId); + log.debug("[{}] Pushing {} pending RPC messages to session: [{}]", deviceId, sessionId, toDeviceRpcPendingMap.size()); if (sessionType == SessionType.SYNC) { - log.debug("[{}] Cleanup sync rpc session [{}]", deviceId, sessionId); + log.debug("[{}] Cleanup sync RPC session [{}]", deviceId, sessionId); rpcSubscriptions.remove(sessionId); } } else { - log.debug("[{}] No pending RPC messages for new async session [{}]", deviceId, sessionId); + log.debug("[{}] No pending RPC messages for session: [{}]", deviceId, sessionId); } Set sentOneWayIds = new HashSet<>(); if (rpcSequential) { - getFirstRpc().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds)); + getFirstRpc().ifPresent(processPendingRpc(sessionId, nodeId, sentOneWayIds)); } else if (sessionType == SessionType.ASYNC) { - toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, nodeId, sentOneWayIds)); + toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(sessionId, nodeId, sentOneWayIds)); } else { - toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds)); + toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(sessionId, nodeId, sentOneWayIds)); } sentOneWayIds.stream().filter(id -> !toDeviceRpcPendingMap.get(id).getMsg().getMsg().isPersisted()).forEach(toDeviceRpcPendingMap::remove); @@ -353,35 +366,38 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso return toDeviceRpcPendingMap.entrySet().stream().filter(e -> !e.getValue().isDelivered()).findFirst(); } - private void sendNextPendingRequest(TbActorCtx context) { + private void sendNextPendingRequest() { if (rpcSequential) { - rpcSubscriptions.forEach((id, s) -> sendPendingRequests(context, id, s.getNodeId())); + rpcSubscriptions.forEach((id, s) -> sendPendingRequests(id, s.getNodeId())); } } - private Consumer> processPendingRpc(TbActorCtx context, UUID sessionId, String nodeId, Set sentOneWayIds) { + private Consumer> processPendingRpc(UUID sessionId, String nodeId, Set sentOneWayIds) { return entry -> { ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg(); ToDeviceRpcRequestBody body = request.getBody(); + Integer requestId = entry.getKey(); + UUID rpcId = request.getId(); if (request.isOneway() && !rpcSequential) { - sentOneWayIds.add(entry.getKey()); - systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(request.getId(), null, null)); + sentOneWayIds.add(requestId); + systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null)); } ToDeviceRpcRequestMsg rpcRequest = ToDeviceRpcRequestMsg.newBuilder() - .setRequestId(entry.getKey()) + .setRequestId(requestId) .setMethodName(body.getMethod()) .setParams(body.getParams()) .setExpirationTime(request.getExpirationTime()) - .setRequestIdMSB(request.getId().getMostSignificantBits()) - .setRequestIdLSB(request.getId().getLeastSignificantBits()) + .setRequestIdMSB(rpcId.getMostSignificantBits()) + .setRequestIdLSB(rpcId.getLeastSignificantBits()) .setOneway(request.isOneway()) .setPersisted(request.isPersisted()) .build(); + log.debug("[{}][{}][{}][{}] Send pending RPC request to transport ...", deviceId, sessionId, rpcId, requestId); sendToTransport(rpcRequest, sessionId, nodeId); }; } - void process(TbActorCtx context, TransportToDeviceActorMsgWrapper wrapper) { + void process(TransportToDeviceActorMsgWrapper wrapper) { TransportToDeviceActorMsg msg = wrapper.getMsg(); TbCallback callback = wrapper.getCallback(); var sessionInfo = msg.getSessionInfo(); @@ -390,36 +406,36 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso processSessionStateMsgs(sessionInfo, msg.getSessionEvent()); } if (msg.hasSubscribeToAttributes()) { - processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToAttributes()); + processSubscriptionCommands(sessionInfo, msg.getSubscribeToAttributes()); } if (msg.hasSubscribeToRPC()) { - processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToRPC()); + processSubscriptionCommands(sessionInfo, msg.getSubscribeToRPC()); } if (msg.hasSendPendingRPC()) { - sendPendingRequests(context, getSessionId(sessionInfo), sessionInfo.getNodeId()); + sendPendingRequests(getSessionId(sessionInfo), sessionInfo.getNodeId()); } if (msg.hasGetAttributes()) { - handleGetAttributesRequest(context, sessionInfo, msg.getGetAttributes()); + handleGetAttributesRequest(sessionInfo, msg.getGetAttributes()); } if (msg.hasToDeviceRPCCallResponse()) { - processRpcResponses(context, sessionInfo, msg.getToDeviceRPCCallResponse()); + processRpcResponses(sessionInfo, msg.getToDeviceRPCCallResponse()); } if (msg.hasSubscriptionInfo()) { - handleSessionActivity(context, sessionInfo, msg.getSubscriptionInfo()); + handleSessionActivity(sessionInfo, msg.getSubscriptionInfo()); } if (msg.hasClaimDevice()) { - handleClaimDeviceMsg(context, sessionInfo, msg.getClaimDevice()); + handleClaimDeviceMsg(msg.getClaimDevice()); } if (msg.hasRpcResponseStatusMsg()) { - processRpcResponseStatus(context, sessionInfo, msg.getRpcResponseStatusMsg()); + processRpcResponseStatus(sessionInfo, msg.getRpcResponseStatusMsg()); } if (msg.hasUplinkNotificationMsg()) { - processUplinkNotificationMsg(context, sessionInfo, msg.getUplinkNotificationMsg()); + processUplinkNotificationMsg(sessionInfo, msg.getUplinkNotificationMsg()); } callback.onSuccess(); } - private void processUplinkNotificationMsg(TbActorCtx context, SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) { + private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) { String nodeId = sessionInfo.getNodeId(); sessions.entrySet().stream() .filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC())) @@ -433,7 +449,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso }); } - private void handleClaimDeviceMsg(TbActorCtx context, SessionInfoProto sessionInfo, ClaimDeviceMsg msg) { + private void handleClaimDeviceMsg(ClaimDeviceMsg msg) { DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); systemContext.getClaimDevicesService().registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); } @@ -446,7 +462,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId); } - private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { + private void handleGetAttributesRequest(SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { int requestId = request.getRequestId(); if (request.getOnlyShared()) { Futures.addCallback(findAllAttributesByScope(DataConstants.SHARED_SCOPE), new FutureCallback<>() { @@ -530,7 +546,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso return sessions.containsKey(sessionId) ? SessionType.ASYNC : SessionType.SYNC; } - void processAttributesUpdate(TbActorCtx context, DeviceAttributesEventNotificationMsg msg) { + void processAttributesUpdate(DeviceAttributesEventNotificationMsg msg) { if (attributeSubscriptions.size() > 0) { boolean hasNotificationData = false; AttributeUpdateNotificationMsg.Builder notification = AttributeUpdateNotificationMsg.newBuilder(); @@ -567,19 +583,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } } - private void processRpcResponses(TbActorCtx context, SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg responseMsg) { + private void processRpcResponses(SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg responseMsg) { UUID sessionId = getSessionId(sessionInfo); - log.debug("[{}] Processing rpc command response [{}]", deviceId, sessionId); - ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId()); + log.debug("[{}][{}] Processing RPC command response: {}", deviceId, sessionId, responseMsg); + int requestId = responseMsg.getRequestId(); + ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(requestId); boolean success = requestMd != null; if (success) { + ToDeviceRpcRequest toDeviceRequestMsg = requestMd.getMsg().getMsg(); + boolean delivered = requestMd.isDelivered(); boolean hasError = StringUtils.isNotEmpty(responseMsg.getError()); try { String payload = hasError ? responseMsg.getError() : responseMsg.getPayload(); systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor( - new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(), - payload, null)); - if (requestMd.getMsg().getMsg().isPersisted()) { + new FromDeviceRpcResponse(toDeviceRequestMsg.getId(), payload, null)); + if (toDeviceRequestMsg.isPersisted()) { RpcStatus status = hasError ? RpcStatus.FAILED : RpcStatus.SUCCESSFUL; JsonNode response; try { @@ -587,28 +605,33 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } catch (IllegalArgumentException e) { response = JacksonUtil.newObjectNode().put("error", payload); } - systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), status, response); + systemContext.getTbRpcService().save(tenantId, new RpcId(toDeviceRequestMsg.getId()), status, response); } } finally { - if (hasError && !requestMd.isDelivered()) { - sendNextPendingRequest(context); + if (!delivered) { + String errorResponse = hasError ? "error" : ""; + log.debug("[{}][{}][{}] Received {} response for undelivered RPC! Going to send next pending request ...", deviceId, sessionId, requestId, errorResponse); + sendNextPendingRequest(); } } } else { - log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId()); + log.debug("[{}][{}][{}] RPC command response is stale!", deviceId, sessionId, requestId); } } - private void processRpcResponseStatus(TbActorCtx context, SessionInfoProto sessionInfo, ToDeviceRpcResponseStatusMsg responseMsg) { + private void processRpcResponseStatus(SessionInfoProto sessionInfo, ToDeviceRpcResponseStatusMsg responseMsg) { UUID rpcId = new UUID(responseMsg.getRequestIdMSB(), responseMsg.getRequestIdLSB()); RpcStatus status = RpcStatus.valueOf(responseMsg.getStatus()); - ToDeviceRpcRequestMetadata md = toDeviceRpcPendingMap.get(responseMsg.getRequestId()); + UUID sessionId = getSessionId(sessionInfo); + int requestId = responseMsg.getRequestId(); + log.debug("[{}][{}][{}][{}] Processing RPC command response status: [{}]", deviceId, sessionId, rpcId, requestId, status); + ToDeviceRpcRequestMetadata md = toDeviceRpcPendingMap.get(requestId); if (md != null) { JsonNode response = null; if (status.equals(RpcStatus.DELIVERED)) { if (md.getMsg().getMsg().isOneway()) { - toDeviceRpcPendingMap.remove(responseMsg.getRequestId()); + toDeviceRpcPendingMap.remove(requestId); if (rpcSequential) { systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null)); } @@ -619,7 +642,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso Integer maxRpcRetries = md.getMsg().getMsg().getRetries(); maxRpcRetries = maxRpcRetries == null ? systemContext.getMaxRpcRetries() : Math.min(maxRpcRetries, systemContext.getMaxRpcRetries()); if (maxRpcRetries <= md.getRetries()) { - toDeviceRpcPendingMap.remove(responseMsg.getRequestId()); + toDeviceRpcPendingMap.remove(requestId); status = RpcStatus.FAILED; response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry attempts have been exhausted. Retry attempts set: " + maxRpcRetries); } else { @@ -631,17 +654,18 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), status, response); } if (status != RpcStatus.SENT) { - sendNextPendingRequest(context); + log.debug("[{}][{}][{}][{}] RPC was {}! Going to send next pending request ...", deviceId, sessionId, rpcId, requestId, status.name().toLowerCase()); + sendNextPendingRequest(); } } else { - log.info("[{}][{}] Rpc has already removed from pending map.", deviceId, rpcId); + log.warn("[{}][{}][{}][{}] RPC has already been removed from pending map.", deviceId, sessionId, rpcId, requestId); } } - private void processSubscriptionCommands(TbActorCtx context, SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) { + private void processSubscriptionCommands(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) { UUID sessionId = getSessionId(sessionInfo); if (subscribeCmd.getUnsubscribe()) { - log.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId); + log.debug("[{}] Canceling attributes subscription for session: [{}]", deviceId, sessionId); attributeSubscriptions.remove(sessionId); } else { SessionInfoMetaData sessionMD = sessions.get(sessionId); @@ -649,7 +673,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId())); } sessionMD.setSubscribedToAttributes(true); - log.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId); + log.debug("[{}] Registering attributes subscription for session: [{}]", deviceId, sessionId); attributeSubscriptions.put(sessionId, sessionMD.getSessionInfo()); dumpSessions(); } @@ -659,10 +683,10 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso return new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); } - private void processSubscriptionCommands(TbActorCtx context, SessionInfoProto sessionInfo, SubscribeToRPCMsg subscribeCmd) { + private void processSubscriptionCommands(SessionInfoProto sessionInfo, SubscribeToRPCMsg subscribeCmd) { UUID sessionId = getSessionId(sessionInfo); if (subscribeCmd.getUnsubscribe()) { - log.debug("[{}] Canceling rpc subscription for session [{}]", deviceId, sessionId); + log.debug("[{}] Canceling RPC subscription for session: [{}]", deviceId, sessionId); rpcSubscriptions.remove(sessionId); } else { SessionInfoMetaData sessionMD = sessions.get(sessionId); @@ -670,9 +694,9 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId())); } sessionMD.setSubscribedToRPC(true); - log.debug("[{}] Registering rpc subscription for session [{}]", deviceId, sessionId); rpcSubscriptions.put(sessionId, sessionMD.getSessionInfo()); - sendPendingRequests(context, sessionId, sessionInfo.getNodeId()); + log.debug("[{}] Registered RPC subscription for session: [{}] Going to check for pending requests ...", deviceId, sessionId); + sendPendingRequests(sessionId, sessionInfo.getNodeId()); dumpSessions(); } } @@ -682,10 +706,10 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso Objects.requireNonNull(sessionId); if (msg.getEvent() == SessionEvent.OPEN) { if (sessions.containsKey(sessionId)) { - log.debug("[{}] Received duplicate session open event [{}]", deviceId, sessionId); + log.debug("[{}][{}] Received duplicate session open event.", deviceId, sessionId); return; } - log.debug("[{}] Processing new session [{}]. Current sessions size {}", deviceId, sessionId, sessions.size()); + log.debug("[{}] Processing new session: [{}] Current sessions size: {}", deviceId, sessionId, sessions.size()); sessions.put(sessionId, new SessionInfoMetaData(new SessionInfo(SessionType.ASYNC, sessionInfo.getNodeId()))); if (sessions.size() == 1) { @@ -694,7 +718,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); dumpSessions(); } else if (msg.getEvent() == SessionEvent.CLOSED) { - log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId); + log.debug("[{}][{}] Canceling subscriptions for closed session.", deviceId, sessionId); sessions.remove(sessionId); attributeSubscriptions.remove(sessionId); rpcSubscriptions.remove(sessionId); @@ -705,7 +729,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } } - private void handleSessionActivity(TbActorCtx context, SessionInfoProto sessionInfoProto, SubscriptionInfoProto subscriptionInfo) { + private void handleSessionActivity(SessionInfoProto sessionInfoProto, SubscriptionInfoProto subscriptionInfo) { UUID sessionId = getSessionId(sessionInfoProto); Objects.requireNonNull(sessionId); @@ -742,7 +766,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } private void notifyTransportAboutClosedSessionMaxSessionsLimit(UUID sessionId, SessionInfoMetaData sessionMd) { - log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId [{}] sessionMd [{}]", sessionId, sessionMd); + log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId: [{}] sessionMd: [{}]", sessionId, sessionMd); notifyTransportAboutClosedSession(sessionId, sessionMd, "max concurrent sessions limit reached per device!"); } @@ -806,14 +830,6 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso systemContext.getTbCoreToTransportService().process(nodeId, msg); } - private void sendToTransport(ToServerRpcResponseMsg rpcMsg, UUID sessionId, String nodeId) { - ToTransportMsg msg = ToTransportMsg.newBuilder() - .setSessionIdMSB(sessionId.getMostSignificantBits()) - .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setToServerResponse(rpcMsg).build(); - systemContext.getTbCoreToTransportService().process(nodeId, msg); - } - private ListenableFuture saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { ObjectNode body = JacksonUtil.newObjectNode(); body.put("requestId", requestId); @@ -914,14 +930,14 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } log.debug("[{}] Restored session: {}", deviceId, sessionMD); } - log.debug("[{}] Restored sessions: {}, rpc subscriptions: {}, attribute subscriptions: {}", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size()); + log.debug("[{}] Restored sessions: {}, RPC subscriptions: {}, attribute subscriptions: {}", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size()); } private void dumpSessions() { if (systemContext.isLocalCacheType()) { return; } - log.debug("[{}] Dumping sessions: {}, rpc subscriptions: {}, attribute subscriptions: {} to cache", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size()); + log.debug("[{}] Dumping sessions: {}, RPC subscriptions: {}, attribute subscriptions: {} to cache", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size()); List sessionsList = new ArrayList<>(sessions.size()); sessions.forEach((uuid, sessionMD) -> { if (sessionMD.getSessionInfo().getType() == SessionType.SYNC) { diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 84b7757846..11a5895fef 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -49,7 +49,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; -import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -211,10 +210,6 @@ public class TenantActor extends RuleChainManagerActor { log.trace("[{}] Ack message because Rule Engine is disabled", tenantId); tbMsg.getCallback().onSuccess(); } - systemContext.getNotificationRuleProcessor().process(RuleEngineMsgTrigger.builder() - .tenantId(tenantId) - .msg(tbMsg) - .build()); } private void onRuleChainMsg(RuleChainAwareMsg msg) { diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index a418dc268b..793670f0ab 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -77,6 +77,8 @@ public class ThingsboardSecurityConfiguration { protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; + public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; + @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; @@ -134,7 +136,7 @@ public class ThingsboardSecurityConfiguration { protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { List pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, - PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); + PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT)); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); @@ -201,6 +203,7 @@ public class ThingsboardSecurityConfiguration { .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point + .antMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .and() .authorizeRequests() diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 5cf97a7cda..4019efe8ee 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -15,13 +15,24 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; +import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest; +import com.google.api.client.auth.oauth2.ClientParametersAuthentication; +import com.google.api.client.auth.oauth2.TokenResponse; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -32,18 +43,25 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.SystemInfo; import org.thingsboard.server.common.data.UpdateMessage; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.JwtSettings; @@ -56,6 +74,7 @@ import org.thingsboard.server.common.data.sync.vc.VcUtils; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.oauth2.CookieUtils; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -67,15 +86,29 @@ import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsSer import org.thingsboard.server.service.system.SystemInfoService; import org.thingsboard.server.service.update.UpdateService; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static org.thingsboard.server.controller.ControllerConstants.*; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent +@Slf4j @RequestMapping("/api/admin") @RequiredArgsConstructor public class AdminController extends BaseController { + private static final String PREV_URI_PATH_PARAMETER = "prevUri"; + private static final String PREV_URI_COOKIE_NAME = "prev_uri"; + private static final String STATE_COOKIE_NAME = "state"; + private static final String MAIL_SETTINGS_KEY = "mail"; + private final MailService mailService; private final SmsService smsService; private final AdminSettingsService adminSettingsService; @@ -102,6 +135,7 @@ public class AdminController extends BaseController { AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); if (adminSettings.getKey().equals("mail")) { ((ObjectNode) adminSettings.getJsonValue()).remove("password"); + ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); } return adminSettings; } @@ -122,6 +156,7 @@ public class AdminController extends BaseController { if (adminSettings.getKey().equals("mail")) { mailService.updateMailConfiguration(); ((ObjectNode) adminSettings.getJsonValue()).remove("password"); + ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); } else if (adminSettings.getKey().equals("sms")) { smsService.updateSmsConfiguration(); } @@ -188,9 +223,20 @@ public class AdminController extends BaseController { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); adminSettings = checkNotNull(adminSettings); if (adminSettings.getKey().equals("mail")) { - if (!adminSettings.getJsonValue().has("password")) { + if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()){ AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); - ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); + JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken"); + if (refreshToken == null) { + throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL); + } + ObjectNode settings = (ObjectNode) adminSettings.getJsonValue(); + settings.put("refreshToken", refreshToken.asText()); + } + else { + if (!adminSettings.getJsonValue().has("password")) { + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); + ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); + } } String email = getCurrentUser().getEmail(); mailService.sendTestMail(adminSettings.getJsonValue(), email); @@ -362,4 +408,84 @@ public class AdminController extends BaseController { return systemInfoService.getFeaturesInfo(); } + @ApiOperation(value = "Get OAuth2 log in processing URL (getMailProcessingUrl)", notes = "Returns the URL enclosed in " + + "double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " + + "further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET) + @ResponseBody + public String getMailProcessingUrl() throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + return "\"/api/admin/mail/oauth2/code\""; + } + + @ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" + + "provider sends authorization code to specified redirect uri.)" ) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text") + public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { + String state = StringUtils.generateSafeToken(); + if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { + CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180); + } + CookieUtils.addCookie(response, STATE_COOKIE_NAME, state, 180); + + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY), "No Administration mail settings found"); + JsonNode jsonValue = adminSettings.getJsonValue(); + + String clientId = checkNotNull(jsonValue.get("clientId"), "No clientId was configured").asText(); + String authUri = checkNotNull(jsonValue.get("authUri"), "No authorization uri was configured").asText(); + String redirectUri = checkNotNull(jsonValue.get("redirectUri"), "No Redirect uri was configured").asText(); + List scope = JacksonUtil.convertValue(checkNotNull(jsonValue.get("scope"), "No scope was configured"), new TypeReference<>() { + }); + + return "\"" + new AuthorizationCodeRequestUrl(authUri, clientId) + .setScopes(scope) + .setState(state) + .setRedirectUri(redirectUri) + .build() + "\""; + } + + @RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET) + public void codeProcessingUrl( + @RequestParam(value = "code") String code, @RequestParam(value = "state") String state, + HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { + Optional prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME); + Optional cookieState = CookieUtils.getCookie(request, STATE_COOKIE_NAME); + + String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + String prevUri = baseUrl + (prevUrlOpt.isPresent() ? prevUrlOpt.get().getValue(): "/settings/outgoing-mail"); + + if (cookieState.isEmpty() || !cookieState.get().getValue().equals(state)) { + CookieUtils.deleteCookie(request, response, STATE_COOKIE_NAME); + throw new ThingsboardException("Refresh token was not generated, invalid state param", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + CookieUtils.deleteCookie(request, response, STATE_COOKIE_NAME); + CookieUtils.deleteCookie(request, response, PREV_URI_COOKIE_NAME); + + AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY), "No Administration mail settings found"); + JsonNode jsonValue = adminSettings.getJsonValue(); + + String clientId = checkNotNull(jsonValue.get("clientId"), "No clientId was configured").asText(); + String clientSecret = checkNotNull(jsonValue.get("clientSecret"), "No client secret was configured").asText(); + String clientRedirectUri = checkNotNull(jsonValue.get("redirectUri"), "No Redirect uri was configured").asText(); + String tokenUri = checkNotNull(jsonValue.get("tokenUri"), "No authorization uri was configured").asText(); + + TokenResponse tokenResponse; + try { + tokenResponse = new AuthorizationCodeTokenRequest(new NetHttpTransport(), new GsonFactory(), new GenericUrl(tokenUri), code) + .setRedirectUri(clientRedirectUri) + .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) + .execute(); + } catch (IOException e) { + log.warn("Unable to retrieve refresh token: {}", e.getMessage()); + throw new ThingsboardException("Error while requesting access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL); + } + ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); + ((ObjectNode)jsonValue).put("tokenGenerated", true); + + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings); + response.sendRedirect(prevUri); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java index 92b2cb923f..12195e83bc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java @@ -77,7 +77,7 @@ public class AlarmCommentController extends BaseController { @PathVariable(ALARM_ID) String strAlarmId, @ApiParam(value = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + Alarm alarm = checkAlarmInfoId(alarmId, Operation.WRITE); alarmComment.setAlarmId(alarmId); return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser()); } diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index f425bde86c..1cb794c2ec 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -140,6 +140,9 @@ public class ControllerConstants { protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title."; protected static final String RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, resourceType, tenantId"; + protected static final String RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES = "LWM2M_MODEL, JKS, PKCS_12, JS_MODULE"; + protected static final String RESOURCE_TYPE = "A string value representing the resource type."; + protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. "; protected static final String LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name"; diff --git a/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java new file mode 100644 index 0000000000..09aed4fee3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java @@ -0,0 +1,56 @@ +/** + * 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. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.mail.TbMailConfigTemplateService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.io.IOException; + +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; + +@RestController +@TbCoreComponent +@RequiredArgsConstructor +@RequestMapping("/api/mail/config/template") +@Slf4j +public class MailConfigTemplateController extends BaseController { + private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports"; + private final TbMailConfigTemplateService mailConfigTemplateService; + + @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + notes = MAIL_CONFIG_TEMPLATE_DEFINITION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public JsonNode getClientRegistrationTemplates() throws ThingsboardException, IOException { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + return mailConfigTemplateService.findAllMailConfigTemplates(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 3ba1df8c2a..0e79ae7955 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -83,7 +83,7 @@ public class QueueController extends BaseController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("serviceType", serviceType); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - ServiceType type = ServiceType.valueOf(serviceType); + ServiceType type = ServiceType.of(serviceType); switch (type) { case TB_RULE_ENGINE: return queueService.findQueuesByTenantId(getTenantId(), pageLink); @@ -136,7 +136,7 @@ public class QueueController extends BaseController { checkEntity(queue.getId(), queue, Resource.QUEUE); - ServiceType type = ServiceType.valueOf(serviceType); + ServiceType type = ServiceType.of(serviceType); switch (type) { case TB_RULE_ENGINE: queue.setTenantId(getTenantId()); diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index d94dc31fa1..94e318f37c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -20,19 +20,25 @@ import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; @@ -47,6 +53,7 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.Base64; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; @@ -57,6 +64,8 @@ import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_ID_ import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; @@ -71,6 +80,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @RequiredArgsConstructor public class TbResourceController extends BaseController { + private static final String DOWNLOAD_RESOURCE_IF_NOT_CHANGED = "Download Resource based on the provided Resource Id or return 304 status code if resource was not changed."; private final TbResourceService tbResourceService; public static final String RESOURCE_ID = "resourceId"; @@ -94,6 +104,47 @@ public class TbResourceController extends BaseController { .body(resource); } + @ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/lwm2m/{resourceId}/download", method = RequestMethod.GET, produces = "application/xml") + @ResponseBody + public ResponseEntity downloadLwm2mResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag); + } + + @ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/pkcs12/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-pkcs12") + @ResponseBody + public ResponseEntity downloadPkcs12ResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag); + } + + @ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)", + notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/jks/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-java-keystore") + @ResponseBody + public ResponseEntity downloadJksResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag); + } + + @ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/resource/js/{resourceId}/download", method = RequestMethod.GET, produces = "application/javascript") + @ResponseBody + public ResponseEntity downloadJsResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag); + } + @ApiOperation(value = "Get Resource Info (getResourceInfoById)", notes = "Fetch the Resource Info object based on the provided Resource Id. " + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, @@ -153,6 +204,8 @@ public class TbResourceController extends BaseController { @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = RESOURCE_TYPE, allowableValues = RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String resourceType, @ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -160,10 +213,15 @@ public class TbResourceController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TbResourceInfoFilter.TbResourceInfoFilterBuilder filter = TbResourceInfoFilter.builder(); + filter.tenantId(getTenantId()); + if (StringUtils.isNotEmpty(resourceType)) { + filter.resourceType(ResourceType.valueOf(resourceType)); + } if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - return checkNotNull(resourceService.findTenantResourcesByTenantId(getTenantId(), pageLink)); + return checkNotNull(resourceService.findTenantResourcesByTenantId(filter.build(), pageLink)); } else { - return checkNotNull(resourceService.findAllTenantResourcesByTenantId(getTenantId(), pageLink)); + return checkNotNull(resourceService.findAllTenantResourcesByTenantId(filter.build(), pageLink)); } } @@ -216,4 +274,30 @@ public class TbResourceController extends BaseController { TbResource tbResource = checkResourceId(resourceId, Operation.DELETE); tbResourceService.delete(tbResource, getCurrentUser()); } + + private ResponseEntity downloadResourceIfChanged(ResourceType type, String strResourceId, String etag) throws ThingsboardException { + checkParameter(RESOURCE_ID, strResourceId); + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + + if (etag != null) { + TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ); + if (etag.equals(tbResourceInfo.getEtag())) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED) + .eTag(tbResourceInfo.getEtag()) + .build(); + } + } + + TbResource tbResource = checkResourceId(resourceId, Operation.READ); + ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes())); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) + .header("x-filename", tbResource.getFileName()) + .contentLength(resource.contentLength()) + .header("Content-Type", type.getMediaType()) + .cacheControl(CacheControl.noCache()) + .eTag(tbResource.getEtag()) + .body(resource); + } } \ 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 f79f5ee782..d49b2b04df 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -406,7 +406,7 @@ public class UserController extends BaseController { public void setUserCredentialsEnabled( @ApiParam(value = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId, - @ApiParam(value = "Disable (\"true\") or enable (\"false\") the credentials.", defaultValue = "true") + @ApiParam(value = "Enable (\"true\") or disable (\"false\") the credentials.", defaultValue = "true") @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException { checkParameter(USER_ID, strUserId); UserId userId = new UserId(toUUID(strUserId)); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 9ce5356d02..554e7d721f 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -256,6 +256,7 @@ public class ThingsboardInstallService { } case "3.5.0": log.info("Upgrading ThingsBoard from version 3.5.0 to 3.5.1 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.5.0"); case "3.5.1": log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.5.1"); diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 623b95b40e..99b508a12d 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.CustomerId; @@ -40,10 +42,12 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.notification.trigger.AlarmAssignmentTrigger; +import org.thingsboard.server.common.msg.notification.trigger.AlarmCommentTrigger; import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger; import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import java.util.List; import java.util.Map; @@ -241,19 +245,7 @@ public class EntityActionService { } } if (tenantId != null && !tenantId.isSysTenantId()) { - if (actionType == ActionType.ADDED) { - notificationRuleProcessor.process(EntitiesLimitTrigger.builder() - .tenantId(tenantId) - .entityType(entityId.getEntityType()) - .build()); - } - notificationRuleProcessor.process(EntityActionTrigger.builder() - .tenantId(tenantId) - .entityId(entityId) - .entity(entity) - .actionType(actionType) - .user(user) - .build()); + processNotificationRules(tenantId, entityId, entity, actionType, user, additionalInfo); } TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null); @@ -263,6 +255,53 @@ public class EntityActionService { } } + private void processNotificationRules(TenantId tenantId, EntityId entityId, HasName entity, ActionType actionType, User user, Object... additionalInfo) { + switch (actionType) { + case ADDED: + notificationRuleProcessor.process(EntitiesLimitTrigger.builder() + .tenantId(tenantId) + .entityType(entityId.getEntityType()) + .build()); + case UPDATED: + case DELETED: + notificationRuleProcessor.process(EntityActionTrigger.builder() + .tenantId(tenantId) + .entityId(entityId) + .entity(entity) + .actionType(actionType) + .user(user) + .build()); + break; + case ALARM_ASSIGNED: + case ALARM_UNASSIGNED: + if (!(entity instanceof AlarmInfo)) { // should not normally happen + log.warn("Invalid alarm assignment event: entity is not instance of AlarmInfo"); + break; + } + notificationRuleProcessor.process(AlarmAssignmentTrigger.builder() + .tenantId(tenantId) + .alarmInfo((AlarmInfo) entity) + .actionType(actionType) + .user(user) + .build()); + break; + case ADDED_COMMENT: + case UPDATED_COMMENT: + if (!(entity instanceof Alarm)) { // should not normally happen + log.warn("Invalid alarm comment event: entity is not instance of Alarm"); + break; + } + notificationRuleProcessor.process(AlarmCommentTrigger.builder() + .tenantId(tenantId) + .comment(extractParameter(AlarmComment.class, 0, additionalInfo)) + .alarm((Alarm) entity) + .actionType(actionType) + .user(user) + .build()); + break; + } + } + public void logEntityAction(User user, I entityId, E entity, CustomerId customerId, ActionType actionType, Exception e, Object... additionalInfo) { if (customerId == null || customerId.isNullUid()) { diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 9e17641c69..7e2719598c 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -61,7 +61,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.service.apiusage.BaseApiUsageState.StatsCalculationResult; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.mail.MailExecutorService; diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 39b533cfc5..34515f827d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -638,7 +638,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService futures.add(dbUpgradeExecutor.submit(() -> { try { assetProfileService.createDefaultAssetProfile(tenantId); - } catch (Exception e) {} + } catch (Exception e) { + } })); } Futures.allAsList(futures).get(); @@ -657,7 +658,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService futures.add(dbUpgradeExecutor.submit(() -> { try { assetProfileService.findOrCreateAssetProfile(tenantId, assetType); - } catch (Exception e) {} + } catch (Exception e) { + } })); } } @@ -714,19 +716,33 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; - case "3.5.1": + case "3.5.0": try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { log.info("Updating schema ..."); if (isOldSchema(conn, 3005000)) { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.0", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005001;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; + case "3.5.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3005001)) { schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.1", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); - try { - String[] entityNames = new String[]{"device"}; - for (String entityName : entityNames) { - conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN search_text CASCADE"); + String[] entityNames = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package", + "asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"}; + for (String entityName : entityNames) { + try { + conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN " + SEARCH_TEXT + " CASCADE"); + } catch (Exception e) { } - } catch (Exception e) { } try { conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;"); diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index f3fd18141a..1c6af41360 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.mail; import com.fasterxml.jackson.databind.JsonNode; + import freemarker.template.Configuration; import freemarker.template.Template; import lombok.extern.slf4j.Slf4j; @@ -53,7 +54,6 @@ import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -61,7 +61,6 @@ import java.util.concurrent.TimeoutException; @Slf4j public class DefaultMailService implements MailService { - public static final String MAIL_PROP = "mail."; public static final String TARGET_EMAIL = "targetEmail"; public static final String UTF_8 = "UTF-8"; @@ -82,7 +81,10 @@ public class DefaultMailService implements MailService { @Autowired private PasswordResetExecutorService passwordResetExecutorService; - private JavaMailSenderImpl mailSender; + @Autowired + private TbMailContextComponent tbMailContextComponent; + + private TbMailSender mailSender; private String mailFrom; @@ -105,7 +107,7 @@ public class DefaultMailService implements MailService { AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); if (settings != null) { JsonNode jsonConfig = settings.getJsonValue(); - mailSender = createMailSender(jsonConfig); + mailSender = new TbMailSender(tbMailContextComponent, jsonConfig); mailFrom = jsonConfig.get("mailFrom").asText(); timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT); } else { @@ -113,65 +115,6 @@ public class DefaultMailService implements MailService { } } - private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) { - JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); - mailSender.setHost(jsonConfig.get("smtpHost").asText()); - mailSender.setPort(parsePort(jsonConfig.get("smtpPort").asText())); - mailSender.setUsername(jsonConfig.get("username").asText()); - mailSender.setPassword(jsonConfig.get("password").asText()); - mailSender.setJavaMailProperties(createJavaMailProperties(jsonConfig)); - return mailSender; - } - - private Properties createJavaMailProperties(JsonNode jsonConfig) { - Properties javaMailProperties = new Properties(); - String protocol = jsonConfig.get("smtpProtocol").asText(); - javaMailProperties.put("mail.transport.protocol", protocol); - javaMailProperties.put(MAIL_PROP + protocol + ".host", jsonConfig.get("smtpHost").asText()); - javaMailProperties.put(MAIL_PROP + protocol + ".port", jsonConfig.get("smtpPort").asText()); - javaMailProperties.put(MAIL_PROP + protocol + ".timeout", jsonConfig.get("timeout").asText()); - javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(jsonConfig.get("username").asText()))); - boolean enableTls = false; - if (jsonConfig.has("enableTls")) { - if (jsonConfig.get("enableTls").isBoolean() && jsonConfig.get("enableTls").booleanValue()) { - enableTls = true; - } else if (jsonConfig.get("enableTls").isTextual()) { - enableTls = "true".equalsIgnoreCase(jsonConfig.get("enableTls").asText()); - } - } - javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", enableTls); - if (enableTls && jsonConfig.has("tlsVersion") && !jsonConfig.get("tlsVersion").isNull()) { - String tlsVersion = jsonConfig.get("tlsVersion").asText(); - if (StringUtils.isNoneEmpty(tlsVersion)) { - javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", tlsVersion); - } - } - - boolean enableProxy = jsonConfig.has("enableProxy") && jsonConfig.get("enableProxy").asBoolean(); - - if (enableProxy) { - javaMailProperties.put(MAIL_PROP + protocol + ".proxy.host", jsonConfig.get("proxyHost").asText()); - javaMailProperties.put(MAIL_PROP + protocol + ".proxy.port", jsonConfig.get("proxyPort").asText()); - String proxyUser = jsonConfig.get("proxyUser").asText(); - if (StringUtils.isNoneEmpty(proxyUser)) { - javaMailProperties.put(MAIL_PROP + protocol + ".proxy.user", proxyUser); - } - String proxyPassword = jsonConfig.get("proxyPassword").asText(); - if (StringUtils.isNoneEmpty(proxyPassword)) { - javaMailProperties.put(MAIL_PROP + protocol + ".proxy.password", proxyPassword); - } - } - return javaMailProperties; - } - - private int parsePort(String strPort) { - try { - return Integer.valueOf(strPort); - } catch (NumberFormatException e) { - throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort)); - } - } - @Override public void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException { sendMail(mailSender, mailFrom, email, subject, message, timeout); @@ -179,7 +122,7 @@ public class DefaultMailService implements MailService { @Override public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException { - JavaMailSenderImpl testMailSender = createMailSender(jsonConfig); + TbMailSender testMailSender = new TbMailSender(tbMailContextComponent, jsonConfig); String mailFrom = jsonConfig.get("mailFrom").asText(); String subject = messages.getMessage("test.message.subject", null, Locale.US); long timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT); diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java new file mode 100644 index 0000000000..cb502e7e51 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java @@ -0,0 +1,42 @@ +/** + * 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. + */ +package org.thingsboard.server.service.mail; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; + +import javax.annotation.PostConstruct; +import java.io.IOException; + +@Service +@Slf4j +public class DefaultTbMailConfigTemplateService implements TbMailConfigTemplateService { + + private JsonNode mailConfigTemplates; + + @PostConstruct + private void postConstruct() throws IOException { + mailConfigTemplates = JacksonUtil.toJsonNode(new ClassPathResource("/templates/mail_config_templates.json").getFile()); + } + + @Override + public JsonNode findAllMailConfigTemplates() { + return mailConfigTemplates; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java b/application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java new file mode 100644 index 0000000000..56f3c4c4c5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java @@ -0,0 +1,75 @@ +/** + * 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. + */ +package org.thingsboard.server.service.mail; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.api.client.auth.oauth2.ClientParametersAuthentication; +import com.google.api.client.auth.oauth2.RefreshTokenRequest; +import com.google.api.client.auth.oauth2.TokenResponse; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; + +import static org.thingsboard.server.common.data.mail.MailOauth2Provider.OFFICE_365; + +@TbCoreComponent +@Service +@Slf4j +@RequiredArgsConstructor +public class RefreshTokenExpCheckService { + public static final int AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS = 90; + private final AdminSettingsService adminSettingsService; + + @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${mail.oauth2.refreshTokenCheckingInterval})}", fixedDelayString = "${mail.oauth2.refreshTokenCheckingInterval}") + public void check() throws IOException { + AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); + if (settings != null && settings.getJsonValue().has("enableOauth2") && settings.getJsonValue().get("enableOauth2").asBoolean()) { + JsonNode jsonValue = settings.getJsonValue(); + if (OFFICE_365.name().equals(jsonValue.get("providerId").asText()) && jsonValue.has("refreshTokenExpires")) { + long expiresIn = jsonValue.get("refreshTokenExpires").longValue(); + if ((expiresIn - System.currentTimeMillis()) < 604800000L) { //less than 7 days + log.info("Trying to refresh refresh token."); + + String clientId = jsonValue.get("clientId").asText(); + String clientSecret = jsonValue.get("clientSecret").asText(); + String refreshToken = jsonValue.get("refreshToken").asText(); + String tokenUri = jsonValue.get("tokenUri").asText(); + + TokenResponse tokenResponse = new RefreshTokenRequest(new NetHttpTransport(), new GsonFactory(), + new GenericUrl(tokenUri), refreshToken) + .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) + .execute(); + ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); + ((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, settings); + } + } + } + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java new file mode 100644 index 0000000000..b69ae0977c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java @@ -0,0 +1,24 @@ +/** + * 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. + */ +package org.thingsboard.server.service.mail; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.IOException; + +public interface TbMailConfigTemplateService { + JsonNode findAllMailConfigTemplates() throws IOException; +} diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java new file mode 100644 index 0000000000..dce6c10b7e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java @@ -0,0 +1,32 @@ +/** + * 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. + */ +package org.thingsboard.server.service.mail; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@Data +@Lazy +public class TbMailContextComponent { + + @Autowired + private AdminSettingsService adminSettingsService; +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java new file mode 100644 index 0000000000..0a9b173247 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java @@ -0,0 +1,168 @@ +/** + * 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. + */ +package org.thingsboard.server.service.mail; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.api.client.auth.oauth2.ClientParametersAuthentication; +import com.google.api.client.auth.oauth2.RefreshTokenRequest; +import com.google.api.client.auth.oauth2.TokenResponse; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mail.MailOauth2Provider; +import org.thingsboard.server.dao.exception.IncorrectParameterException; + +import javax.mail.internet.MimeMessage; +import java.time.Duration; +import java.time.Instant; +import java.util.Properties; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import static org.thingsboard.server.service.mail.RefreshTokenExpCheckService.AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS; + +@Slf4j +public class TbMailSender extends JavaMailSenderImpl { + + private static final String MAIL_PROP = "mail."; + private final TbMailContextComponent ctx; + private final Lock lock; + private final Boolean oauth2Enabled; + private volatile String accessToken; + private volatile long tokenExpires; + + public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) { + super(); + this.lock = new ReentrantLock(); + this.tokenExpires = 0L; + this.ctx = ctx; + this.oauth2Enabled = jsonConfig.has("enableOauth2") && jsonConfig.get("enableOauth2").asBoolean(); + + setHost(jsonConfig.get("smtpHost").asText()); + setPort(parsePort(jsonConfig.get("smtpPort").asText())); + setUsername(jsonConfig.get("username").asText()); + if (jsonConfig.has("password")) { + setPassword(jsonConfig.get("password").asText()); + } + setJavaMailProperties(createJavaMailProperties(jsonConfig)); + } + + @SneakyThrows + @Override + public void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) { + if (oauth2Enabled && (System.currentTimeMillis() > tokenExpires)){ + refreshAccessToken(); + setPassword(accessToken); + } + super.doSend(mimeMessages, originalMessages); + } + + private Properties createJavaMailProperties(JsonNode jsonConfig) { + Properties javaMailProperties = new Properties(); + String protocol = jsonConfig.get("smtpProtocol").asText(); + javaMailProperties.put("mail.transport.protocol", protocol); + javaMailProperties.put(MAIL_PROP + protocol + ".host", jsonConfig.get("smtpHost").asText()); + javaMailProperties.put(MAIL_PROP + protocol + ".port", jsonConfig.get("smtpPort").asText()); + javaMailProperties.put(MAIL_PROP + protocol + ".timeout", jsonConfig.get("timeout").asText()); + javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(jsonConfig.get("username").asText()))); + boolean enableTls = false; + if (jsonConfig.has("enableTls")) { + if (jsonConfig.get("enableTls").isBoolean() && jsonConfig.get("enableTls").booleanValue()) { + enableTls = true; + } else if (jsonConfig.get("enableTls").isTextual()) { + enableTls = "true".equalsIgnoreCase(jsonConfig.get("enableTls").asText()); + } + } + javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", enableTls); + if (enableTls && jsonConfig.has("tlsVersion") && !jsonConfig.get("tlsVersion").isNull()) { + String tlsVersion = jsonConfig.get("tlsVersion").asText(); + if (StringUtils.isNoneEmpty(tlsVersion)) { + javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", tlsVersion); + } + } + + boolean enableProxy = jsonConfig.has("enableProxy") && jsonConfig.get("enableProxy").asBoolean(); + + if (enableProxy) { + javaMailProperties.put(MAIL_PROP + protocol + ".proxy.host", jsonConfig.get("proxyHost").asText()); + javaMailProperties.put(MAIL_PROP + protocol + ".proxy.port", jsonConfig.get("proxyPort").asText()); + String proxyUser = jsonConfig.get("proxyUser").asText(); + if (StringUtils.isNoneEmpty(proxyUser)) { + javaMailProperties.put(MAIL_PROP + protocol + ".proxy.user", proxyUser); + } + String proxyPassword = jsonConfig.get("proxyPassword").asText(); + if (StringUtils.isNoneEmpty(proxyPassword)) { + javaMailProperties.put(MAIL_PROP + protocol + ".proxy.password", proxyPassword); + } + } + + if (oauth2Enabled) { + javaMailProperties.put(MAIL_PROP + protocol + ".auth.mechanisms", "XOAUTH2"); + } + return javaMailProperties; + } + + public void refreshAccessToken() throws ThingsboardException { + lock.lock(); + try { + if (System.currentTimeMillis() > tokenExpires) { + AdminSettings settings = ctx.getAdminSettingsService().findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); + JsonNode jsonValue = settings.getJsonValue(); + + String clientId = jsonValue.get("clientId").asText(); + String clientSecret = jsonValue.get("clientSecret").asText(); + String refreshToken = jsonValue.get("refreshToken").asText(); + String tokenUri = jsonValue.get("tokenUri").asText(); + String providerId = jsonValue.get("providerId").asText(); + + TokenResponse tokenResponse = new RefreshTokenRequest(new NetHttpTransport(), new GsonFactory(), + new GenericUrl(tokenUri), refreshToken) + .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) + .execute(); + if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) { + ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); + ((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); + ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings); + } + accessToken = tokenResponse.getAccessToken(); + tokenExpires = System.currentTimeMillis() + (tokenResponse.getExpiresInSeconds().intValue() * 1000); + } + } catch (Exception e) { + log.warn("Unable to retrieve access token: {}", e.getMessage()); + throw new ThingsboardException("Error while retrieving access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL); + } finally { + lock.unlock(); + } + } + + private int parsePort(String strPort) { + try { + return Integer.parseInt(strPort); + } catch (NumberFormatException e) { + throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort)); + } + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 0cb9dc780b..7215776170 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -15,13 +15,10 @@ */ package org.thingsboard.server.service.notification; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -59,14 +56,12 @@ import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.dao.util.limits.LimitedApi; -import org.thingsboard.server.dao.util.limits.RateLimitService; -import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.notification.channels.NotificationChannel; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; @@ -74,7 +69,6 @@ import org.thingsboard.server.service.telemetry.AbstractSubscriptionService; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; -import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -87,7 +81,7 @@ import java.util.stream.Collectors; @Service @Slf4j @RequiredArgsConstructor -@SuppressWarnings({"UnstableApiUsage", "rawtypes"}) +@SuppressWarnings({"rawtypes"}) public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel { private final NotificationTargetService notificationTargetService; @@ -95,9 +89,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple private final NotificationService notificationService; private final NotificationTemplateService notificationTemplateService; private final NotificationSettingsService notificationSettingsService; - private final UserService userService; private final NotificationExecutorService notificationExecutor; - private final DbCallbackExecutorService dbCallbackExecutorService; private final NotificationsTopicService notificationsTopicService; private final TbQueueProducerProvider producerProvider; private final RateLimitService rateLimitService; @@ -172,37 +164,32 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple .build(); notificationExecutor.submit(() -> { - List> results = new ArrayList<>(); - for (NotificationTarget target : targets) { - List> result = processForTarget(target, ctx); - results.addAll(result); + processForTarget(target, ctx); + } + + NotificationRequestId requestId = ctx.getRequest().getId(); + log.debug("[{}] Notification request processing is finished", requestId); + NotificationRequestStats stats = ctx.getStats(); + try { + notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats); + } catch (Exception e) { + log.error("[{}] Failed to update stats for notification request", requestId, e); } - Futures.whenAllComplete(results).run(() -> { - NotificationRequestId requestId = ctx.getRequest().getId(); - log.debug("[{}] Notification request processing is finished", requestId); - NotificationRequestStats stats = ctx.getStats(); + if (callback != null) { try { - notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats); + callback.accept(stats); } catch (Exception e) { - log.error("[{}] Failed to update stats for notification request", requestId, e); - } - - if (callback != null) { - try { - callback.accept(stats); - } catch (Exception e) { - log.error("Failed to process callback for notification request {}", requestId, e); - } + log.error("Failed to process callback for notification request {}", requestId, e); } - }, dbCallbackExecutorService); + } }); return request; } - private List> processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { + private void processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { Iterable recipients; switch (target.getConfiguration().getType()) { case PLATFORM_USERS: { @@ -231,43 +218,35 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple Set deliveryMethods = new HashSet<>(ctx.getDeliveryMethods()); deliveryMethods.removeIf(deliveryMethod -> !target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod)); log.debug("[{}] Processing notification request for {} target ({}) for delivery methods {}", ctx.getRequest().getId(), target.getConfiguration().getType(), target.getId(), deliveryMethods); + if (deliveryMethods.isEmpty()) { + return; + } - List> results = new ArrayList<>(); - if (!deliveryMethods.isEmpty()) { - for (NotificationRecipient recipient : recipients) { - for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { - ListenableFuture resultFuture = processForRecipient(deliveryMethod, recipient, ctx); - DonAsynchron.withCallback(resultFuture, result -> { - ctx.getStats().reportSent(deliveryMethod, recipient); - }, error -> { - ctx.getStats().reportError(deliveryMethod, error, recipient); - }); - results.add(resultFuture); + for (NotificationRecipient recipient : recipients) { + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + try { + processForRecipient(deliveryMethod, recipient, ctx); + ctx.getStats().reportSent(deliveryMethod, recipient); + } catch (Exception error) { + ctx.getStats().reportError(deliveryMethod, error, recipient); } } } - return results; } - private ListenableFuture processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) { + private void processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) throws Exception { if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { - return Futures.immediateFailedFuture(new AlreadySentException()); + throw new AlreadySentException(); } - - DeliveryMethodNotificationTemplate processedTemplate; - try { - processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); - } catch (Exception e) { - return Futures.immediateFailedFuture(e); - } - NotificationChannel notificationChannel = channels.get(deliveryMethod); + DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); + log.trace("[{}] Sending {} notification for recipient {}", ctx.getRequest().getId(), deliveryMethod, recipient); - return notificationChannel.sendNotification(recipient, processedTemplate, ctx); + notificationChannel.sendNotification(recipient, processedTemplate, ctx); } @Override - public ListenableFuture sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + public void sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { NotificationRequest request = ctx.getRequest(); Notification notification = Notification.builder() .requestId(request.getId()) @@ -283,14 +262,14 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple notification = notificationService.saveNotification(recipient.getTenantId(), notification); } catch (Exception e) { log.error("Failed to create notification for recipient {}", recipient.getId(), e); - return Futures.immediateFailedFuture(e); + throw e; } NotificationUpdate update = NotificationUpdate.builder() .created(true) .notification(notification) .build(); - return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update); + onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update); } @Override @@ -384,13 +363,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple clusterService.pushMsgToCore(tenantId, notificationRequestId, toCoreMsg, null); } - private ListenableFuture onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) { + private void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) { log.trace("Submitting notification update for recipient {}: {}", recipientId, update); - return Futures.submit(() -> { - forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> { - subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY); - }, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update)); - }, wsCallBackExecutor); + forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> { + subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY); + }, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update)); } private void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate update) { diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java index 8b3c9551c2..8e9e8525e3 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.notification.channels; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.rule.engine.api.MailService; @@ -24,7 +23,6 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; -import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.notification.NotificationProcessingContext; @Component @@ -32,19 +30,15 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext public class EmailNotificationChannel implements NotificationChannel { private final MailService mailService; - private final MailExecutorService executor; @Override - public ListenableFuture sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { - return executor.submit(() -> { - mailService.send(recipient.getTenantId(), null, TbEmail.builder() - .to(recipient.getEmail()) - .subject(processedTemplate.getSubject()) - .body(processedTemplate.getBody()) - .html(true) - .build()); - return null; - }); + public void sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { + mailService.send(recipient.getTenantId(), null, TbEmail.builder() + .to(recipient.getEmail()) + .subject(processedTemplate.getSubject()) + .body(processedTemplate.getBody()) + .html(true) + .build()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java index 02fe6264d2..0275644765 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.notification.channels; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; @@ -24,7 +23,7 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext public interface NotificationChannel { - ListenableFuture sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx); + void sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx) throws Exception; void check(TenantId tenantId) throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java index 46afbd7270..25c6b9dcab 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.notification.channels; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.rule.engine.api.slack.SlackService; @@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.notification.settings.SlackNotificatio import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; import org.thingsboard.server.dao.notification.NotificationSettingsService; -import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.notification.NotificationProcessingContext; @Component @@ -35,15 +33,11 @@ public class SlackNotificationChannel implements NotificationChannel sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + public void sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { SlackNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.SLACK); - return executor.submit(() -> { - slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody()); - return null; - }); + slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java index 3c44dabd4b..44b61d3bb3 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.service.notification.channels; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -26,26 +24,21 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; import org.thingsboard.server.service.notification.NotificationProcessingContext; -import org.thingsboard.server.service.sms.SmsExecutorService; @Component @RequiredArgsConstructor public class SmsNotificationChannel implements NotificationChannel { private final SmsService smsService; - private final SmsExecutorService executor; @Override - public ListenableFuture sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + public void sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { String phone = recipient.getPhone(); if (StringUtils.isBlank(phone)) { - return Futures.immediateFailedFuture(new RuntimeException("User does not have phone number")); + throw new RuntimeException("User does not have phone number"); } - return executor.submit(() -> { - smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody()); - return null; - }); + smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java index 9c57b3cc77..71a59026a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java @@ -15,10 +15,11 @@ */ package org.thingsboard.server.service.notification.rule; -import lombok.Data; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Lazy; @@ -38,34 +39,33 @@ import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.settings.TriggerTypeConfig; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; -import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.util.limits.LimitedApi; import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache; import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor; -import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor; import javax.annotation.PostConstruct; -import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@ConfigurationProperties(prefix = "notification-system.rules") @Slf4j @SuppressWarnings({"rawtypes", "unchecked"}) public class DefaultNotificationRuleProcessor implements NotificationRuleProcessor { @@ -79,6 +79,8 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess private final NotificationExecutorService notificationExecutor; private final CacheManager cacheManager; private Cache sentNotifications; + @Setter + private Map triggerTypesConfigs; private final Map triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class); @@ -93,20 +95,27 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess @Override public void process(NotificationRuleTrigger trigger) { NotificationRuleTriggerType triggerType = trigger.getType(); - if (triggerType == null) return; TenantId tenantId = triggerType.isTenantLevel() ? trigger.getTenantId() : TenantId.SYS_TENANT_ID; try { - List rules = notificationRulesCache.getEnabled(tenantId, triggerType); - for (NotificationRule rule : rules) { - notificationExecutor.submit(() -> { + List enabledRules = notificationRulesCache.getEnabled(tenantId, triggerType); + if (enabledRules.isEmpty()) { + return; + } + if (trigger.deduplicate()) { + enabledRules = new ArrayList<>(enabledRules); + enabledRules.removeIf(rule -> alreadySent(rule, trigger)); + } + final List rules = enabledRules; + notificationExecutor.submit(() -> { + for (NotificationRule rule : rules) { try { processNotificationRule(rule, trigger); } catch (Throwable e) { log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e); } - }); - } + } + }); } catch (Throwable e) { log.error("Failed to process notification rules for trigger: {}", trigger, e); } @@ -142,9 +151,6 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName()); return; } - if (trigger.getType().isDeduplicate() && alreadySent(rule.getId(), trigger)) { - return; - } NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig); rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> { @@ -172,14 +178,13 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess .ruleId(rule.getId()) .originatorEntityId(originatorEntityId) .build(); - notificationExecutor.submit(() -> { - try { - log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets); - notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest, null); - } catch (Exception e) { - log.error("Failed to process notification request for tenant {} for rule {}", rule.getTenantId(), rule.getId(), e); - } - }); + + try { + log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets); + notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest, null); + } catch (Exception e) { + log.error("Failed to process notification request for tenant {} for rule {}", rule.getTenantId(), rule.getId(), e); + } } private boolean matchesFilter(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) { @@ -194,23 +199,34 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(trigger); } - private boolean alreadySent(NotificationRuleId ruleId, NotificationRuleTrigger trigger) { - String key = ruleId + "_" + trigger.getOriginatorEntityId(); - SentNotification sent = sentNotifications.get(key, SentNotification.class); - boolean alreadySent; - if (sent != null && sent.getTrigger().equals(trigger)) { - alreadySent = true; - log.debug("Notification for {} trigger was already sent, ignoring", trigger.getType()); - // updating cache anyway so that the value is not removed by ttl - } else { - alreadySent = false; - sent = new SentNotification(trigger); + private boolean alreadySent(NotificationRule rule, NotificationRuleTrigger trigger) { + String deduplicationKey = getDeduplicationKey(trigger, rule); + + boolean alreadySent = false; + Long lastSentTs = sentNotifications.get(deduplicationKey, Long.class); + if (lastSentTs != null) { + long deduplicationDuration = Optional.ofNullable(triggerTypesConfigs) + .map(triggerTypes -> triggerTypes.get(trigger.getType())) + .map(TriggerTypeConfig::getDeduplicationDuration) + .orElseGet(trigger::getDefaultDeduplicationDuration); + long passed = System.currentTimeMillis() - lastSentTs; + log.trace("Deduplicating trigger {} for rule '{}' by key '{}'. Deduplication duration: {} ms, passed: {} ms", + trigger.getType(), rule.getName(), deduplicationKey, deduplicationDuration, passed); + if (deduplicationDuration == 0 || passed <= deduplicationDuration) { + alreadySent = true; + } + } + if (!alreadySent) { + lastSentTs = System.currentTimeMillis(); } - log.trace("[{}] Putting to sentNotifications cache: {}", ruleId, trigger); - sentNotifications.put(key, sent); + sentNotifications.put(deduplicationKey, lastSentTs); return alreadySent; } + public static String getDeduplicationKey(NotificationRuleTrigger trigger, NotificationRule rule) { + return String.join("_", trigger.getDeduplicationKey(), rule.getDeduplicationKey()); + } + @EventListener(ComponentLifecycleMsg.class) public void onNotificationRuleDeleted(ComponentLifecycleMsg componentLifecycleMsg) { if (componentLifecycleMsg.getEvent() != ComponentLifecycleEvent.DELETED || @@ -232,24 +248,9 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess @Autowired public void setTriggerProcessors(Collection processors) { - Map ruleEngineMsgTypeToTriggerType = new HashMap<>(); processors.forEach(processor -> { triggerProcessors.put(processor.getTriggerType(), processor); - if (processor instanceof RuleEngineMsgNotificationRuleTriggerProcessor) { - Set supportedMsgTypes = ((RuleEngineMsgNotificationRuleTriggerProcessor) processor).getSupportedMsgTypes(); - supportedMsgTypes.forEach(supportedMsgType -> { - ruleEngineMsgTypeToTriggerType.put(supportedMsgType, processor.getTriggerType()); - }); - } }); - RuleEngineMsgTrigger.msgTypeToTriggerType = ruleEngineMsgTypeToTriggerType; - } - - @Data - private static class SentNotification implements Serializable { - private static final long serialVersionUID = 38973480405095422L; - - private final NotificationRuleTrigger trigger; } } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java index a02b7bf1e4..a43ed59efa 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.notification.rule.cache; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -50,7 +49,7 @@ public class DefaultNotificationRulesCache implements NotificationRulesCache { private int cacheMaxSize; @Value("${cache.notificationRules.timeToLiveInMinutes:30}") private int cacheValueTtl; - private Cache> cache; + private Cache> cache; private final ReadWriteLock lock = new ReentrantReadWriteLock(); @@ -96,21 +95,15 @@ public class DefaultNotificationRulesCache implements NotificationRulesCache { } } - private void evict(TenantId tenantId) { + public void evict(TenantId tenantId) { cache.invalidateAll(Arrays.stream(NotificationRuleTriggerType.values()) .map(triggerType -> key(tenantId, triggerType)) .collect(Collectors.toList())); log.trace("Evicted all notification rules for tenant {} from cache", tenantId); } - private static CacheKey key(TenantId tenantId, NotificationRuleTriggerType triggerType) { - return new CacheKey(tenantId, triggerType); - } - - @Data - private static class CacheKey { - private final TenantId tenantId; - private final NotificationRuleTriggerType triggerType; + private static String key(TenantId tenantId, NotificationRuleTriggerType triggerType) { + return tenantId + "_" + triggerType; } } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java index ed72f6be58..eca258aecc 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java @@ -16,52 +16,48 @@ package org.thingsboard.server.service.notification.rule.trigger; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.notification.info.AlarmAssignmentNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig.Action; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; -import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; - -import java.util.Set; +import org.thingsboard.server.common.msg.notification.trigger.AlarmAssignmentTrigger; import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @Service -public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor { +public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerProcessor { @Override - public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { - Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? Action.ASSIGNED : Action.UNASSIGNED; + public boolean matchesFilter(AlarmAssignmentTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { + Action action = trigger.getActionType() == ActionType.ALARM_ASSIGNED ? Action.ASSIGNED : Action.UNASSIGNED; if (!triggerConfig.getNotifyOn().contains(action)) { return false; } - Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class); - return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) && - emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) && - (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); + AlarmInfo alarmInfo = trigger.getAlarmInfo(); + return emptyOrContains(triggerConfig.getAlarmTypes(), alarmInfo.getType()) && + emptyOrContains(triggerConfig.getAlarmSeverities(), alarmInfo.getSeverity()) && + (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarmInfo)); } @Override - public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { - AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class); + public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmAssignmentTrigger trigger) { + AlarmInfo alarmInfo = trigger.getAlarmInfo(); AlarmAssignee assignee = alarmInfo.getAssignee(); return AlarmAssignmentNotificationInfo.builder() - .action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? "assigned" : "unassigned") + .action(trigger.getActionType() == ActionType.ALARM_ASSIGNED ? "assigned" : "unassigned") .assigneeFirstName(assignee != null ? assignee.getFirstName() : null) .assigneeLastName(assignee != null ? assignee.getLastName() : null) .assigneeEmail(assignee != null ? assignee.getEmail() : null) .assigneeId(assignee != null ? assignee.getId() : null) - .userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) - .userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) - .userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) + .userEmail(trigger.getUser().getEmail()) + .userFirstName(trigger.getUser().getFirstName()) + .userLastName(trigger.getUser().getLastName()) .alarmId(alarmInfo.getUuidId()) .alarmType(alarmInfo.getType()) .alarmOriginator(alarmInfo.getOriginator()) @@ -77,9 +73,4 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio return NotificationRuleTriggerType.ALARM_ASSIGNMENT; } - @Override - public Set getSupportedMsgTypes() { - return Set.of(DataConstants.ALARM_ASSIGNED, DataConstants.ALARM_UNASSIGNED); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java index cf71718f83..70024d6e09 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java @@ -15,68 +15,67 @@ */ package org.thingsboard.server.service.notification.rule.trigger; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; - -import java.util.Set; +import org.thingsboard.server.common.msg.notification.trigger.AlarmCommentTrigger; +import org.thingsboard.server.dao.entity.EntityService; import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @Service -public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor { +@RequiredArgsConstructor +public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProcessor { + + private final EntityService entityService; @Override - public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) { - TbMsg msg = trigger.getMsg(); - if (msg.getMetaData().getValue("comment") == null) { - return false; - } - if (msg.getType().equals(DataConstants.COMMENT_UPDATED) && !triggerConfig.isNotifyOnCommentUpdate()) { + public boolean matchesFilter(AlarmCommentTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) { + if (trigger.getActionType() == ActionType.UPDATED_COMMENT && !triggerConfig.isNotifyOnCommentUpdate()) { return false; } if (triggerConfig.isOnlyUserComments()) { - AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class); - if (comment.getType() == AlarmCommentType.SYSTEM) { + if (trigger.getComment().getType() == AlarmCommentType.SYSTEM) { return false; } } - Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class); + Alarm alarm = trigger.getAlarm(); return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) && emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) && (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); } @Override - public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { - TbMsg msg = trigger.getMsg(); - AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class); - AlarmInfo alarmInfo = JacksonUtil.fromString(msg.getData(), AlarmInfo.class); + public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmCommentTrigger trigger) { + Alarm alarm = trigger.getAlarm(); + String originatorName; + if (alarm instanceof AlarmInfo) { + originatorName = ((AlarmInfo) alarm).getOriginatorName(); + } else { + originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse(""); + } return AlarmCommentNotificationInfo.builder() - .comment(comment.getComment().get("text").asText()) - .action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated") - .userEmail(msg.getMetaData().getValue("userEmail")) - .userFirstName(msg.getMetaData().getValue("userFirstName")) - .userLastName(msg.getMetaData().getValue("userLastName")) - .alarmId(alarmInfo.getUuidId()) - .alarmType(alarmInfo.getType()) - .alarmOriginator(alarmInfo.getOriginator()) - .alarmOriginatorName(alarmInfo.getOriginatorName()) - .alarmSeverity(alarmInfo.getSeverity()) - .alarmStatus(alarmInfo.getStatus()) - .alarmCustomerId(alarmInfo.getCustomerId()) + .comment(trigger.getComment().getComment().get("text").asText()) + .action(trigger.getActionType() == ActionType.ADDED_COMMENT ? "added" : "updated") + .userEmail(trigger.getUser().getEmail()) + .userFirstName(trigger.getUser().getFirstName()) + .userLastName(trigger.getUser().getLastName()) + .alarmId(alarm.getUuidId()) + .alarmType(alarm.getType()) + .alarmOriginator(alarm.getOriginator()) + .alarmOriginatorName(originatorName) + .alarmSeverity(alarm.getSeverity()) + .alarmStatus(alarm.getStatus()) + .alarmCustomerId(alarm.getCustomerId()) .build(); } @@ -85,9 +84,4 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu return NotificationRuleTriggerType.ALARM_COMMENT; } - @Override - public Set getSupportedMsgTypes() { - return Set.of(DataConstants.COMMENT_CREATED, DataConstants.COMMENT_UPDATED); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java index 6e181044c7..3188eeabb1 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java @@ -18,9 +18,7 @@ package org.thingsboard.server.service.notification.rule.trigger; import lombok.RequiredArgsConstructor; import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.info.DeviceActivityNotificationInfo; @@ -28,28 +26,22 @@ import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotifi import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; +import org.thingsboard.server.common.msg.notification.trigger.DeviceActivityTrigger; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import java.util.Set; - @Service @RequiredArgsConstructor -public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor { +public class DeviceActivityTriggerProcessor implements NotificationRuleTriggerProcessor { private final TbDeviceProfileCache deviceProfileCache; @Override - public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) { - if (trigger.getMsg().getOriginator().getEntityType() != EntityType.DEVICE) { - return false; - } - DeviceEvent event = trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE; + public boolean matchesFilter(DeviceActivityTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) { + DeviceEvent event = trigger.isActive() ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE; if (!triggerConfig.getNotifyOn().contains(event)) { return false; } - DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator(); + DeviceId deviceId = trigger.getDeviceId(); if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) { return triggerConfig.getDevices().contains(deviceId.getId()); } else if (CollectionUtils.isNotEmpty(triggerConfig.getDeviceProfiles())) { @@ -61,15 +53,14 @@ public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotification } @Override - public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { - TbMsg msg = trigger.getMsg(); + public RuleOriginatedNotificationInfo constructNotificationInfo(DeviceActivityTrigger trigger) { return DeviceActivityNotificationInfo.builder() - .eventType(trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? "active" : "inactive") - .deviceId(msg.getOriginator().getId()) - .deviceName(msg.getMetaData().getValue("deviceName")) - .deviceType(msg.getMetaData().getValue("deviceType")) - .deviceLabel(msg.getMetaData().getValue("deviceLabel")) - .deviceCustomerId(msg.getCustomerId()) + .eventType(trigger.isActive() ? "active" : "inactive") + .deviceId(trigger.getDeviceId().getId()) + .deviceName(trigger.getDeviceName()) + .deviceType(trigger.getDeviceType()) + .deviceLabel(trigger.getDeviceLabel()) + .deviceCustomerId(trigger.getCustomerId()) .build(); } @@ -78,9 +69,4 @@ public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotification return NotificationRuleTriggerType.DEVICE_ACTIVITY; } - @Override - public Set getSupportedMsgTypes() { - return Set.of(DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java deleted file mode 100644 index ac8f692f02..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.service.notification.rule.trigger; - -import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; -import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; - -import java.util.Set; - -public interface RuleEngineMsgNotificationRuleTriggerProcessor extends NotificationRuleTriggerProcessor { - - Set getSupportedMsgTypes(); - -} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 2bd0043046..182fce1c48 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -63,7 +63,7 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; -import org.thingsboard.server.queue.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.DataDecodingEncodingService; diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index b428e1cc05..81a06e9344 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.resource; +import com.google.common.hash.HashCode; +import com.google.common.hash.Hashing; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; @@ -22,6 +24,7 @@ import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -35,6 +38,7 @@ import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import java.util.Base64; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -72,13 +76,13 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements } @Override - public PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { - return resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); + public PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { + return resourceService.findAllTenantResourcesByTenantId(filter, pageLink); } @Override - public PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { - return resourceService.findTenantResourcesByTenantId(tenantId, pageLink); + public PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { + return resourceService.findTenantResourcesByTenantId(filter, pageLink); } @Override @@ -165,6 +169,8 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements } else { resource.setResourceKey(resource.getFileName()); } + HashCode hashCode = Hashing.sha256().hashBytes(Base64.getDecoder().decode(resource.getData().getBytes())); + resource.setEtag(hashCode.toString()); return resourceService.saveResource(resource); } } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index 024d391482..40fe7dda9a 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.resource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; @@ -35,9 +36,9 @@ public interface TbResourceService extends SimpleTbEntityService { TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); - PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); - PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); List findLwM2mObject(TenantId tenantId, String sortOrder, diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java index 4deccb35a2..365eefafbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java @@ -15,22 +15,30 @@ */ package org.thingsboard.server.service.security.auth.oauth2; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.SerializationUtils; +import org.springframework.security.jackson2.SecurityJackson2Modules; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectStreamClass; +import java.util.Arrays; import java.util.Base64; import java.util.Optional; @Slf4j public class CookieUtils { + private static final ObjectMapper OBJECT_MAPPER; + + static { + ClassLoader loader = CookieUtils.class.getClassLoader(); + OBJECT_MAPPER = new ObjectMapper(); + OBJECT_MAPPER.registerModules(SecurityJackson2Modules.getModules(loader)); + } + public static Optional getCookie(HttpServletRequest request, String name) { Cookie[] cookies = request.getCookies(); @@ -56,7 +64,7 @@ public class CookieUtils { public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { Cookie[] cookies = request.getCookies(); if (cookies != null && cookies.length > 0) { - for (Cookie cookie: cookies) { + for (Cookie cookie : cookies) { if (cookie.getName().equals(name)) { cookie.setValue(""); cookie.setPath("/"); @@ -68,27 +76,22 @@ public class CookieUtils { } public static String serialize(Object object) { - return Base64.getUrlEncoder() - .encodeToString(SerializationUtils.serialize(object)); + try { + return Base64.getUrlEncoder() + .encodeToString(OBJECT_MAPPER.writeValueAsBytes(object)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("The given Json object value: " + + object + " cannot be transformed to a String", e); + } } public static T deserialize(Cookie cookie, Class cls) { byte[] decodedBytes = Base64.getUrlDecoder().decode(cookie.getValue()); - try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decodedBytes)) { - @Override - protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { - String name = desc.getName(); - if (!cls.getName().equals(name)) { - throw new ClassNotFoundException("Class not allowed for deserialization: " + name); - } - return super.resolveClass(desc); - } - }) { - - return cls.cast(ois.readObject()); - } catch (Exception e) { - log.debug("Failed to deserialize class from cookie.", e.getCause()); - return null; + try { + return OBJECT_MAPPER.readValue(decodedBytes, cls); + } catch (IOException e) { + throw new IllegalArgumentException("The given string value: " + + Arrays.toString(decodedBytes) + " cannot be transformed to Json object", e); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index 1a8f87713e..bcd1c287c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -19,9 +19,13 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; @@ -44,6 +48,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.RPC, rpcPermissionChecker); put(Resource.DEVICE_PROFILE, profilePermissionChecker); put(Resource.ASSET_PROFILE, profilePermissionChecker); + put(Resource.TB_RESOURCE, customerResourcePermissionChecker); } private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() { @@ -95,6 +100,26 @@ public class CustomerUserPermissions extends AbstractPermissions { }; + private static final PermissionChecker customerResourcePermissionChecker = + new PermissionChecker() { + + @Override + @SuppressWarnings("unchecked") + public boolean hasPermission(SecurityUser user, Operation operation, TbResourceId resourceId, TbResourceInfo resource) { + if (operation != Operation.READ) { + return false; + } + if (resource.getResourceType() == null || !resource.getResourceType().isCustomerAccess()) { + return false; + } + if (resource.getTenantId() == null || resource.getTenantId().isNullUid()) { + return true; + } + return user.getTenantId().equals(resource.getTenantId()); + } + + }; + private static final PermissionChecker customerDashboardPermissionChecker = new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index e33e501fbc..337d607fac 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -31,7 +31,6 @@ import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; @@ -63,6 +62,8 @@ import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.notification.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -70,12 +71,10 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.sql.query.EntityQueryRepository; -import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.util.DbTypeInfoComponent; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.partition.AbstractPartitionBasedService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; @@ -158,6 +157,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService + + + diff --git a/application/src/main/resources/templates/mail_config_templates.json b/application/src/main/resources/templates/mail_config_templates.json new file mode 100644 index 0000000000..f287880a5e --- /dev/null +++ b/application/src/main/resources/templates/mail_config_templates.json @@ -0,0 +1,52 @@ +[ + { + "providerId": "SENDGRID", + "smtpProtocol": "SMTPS", + "smtpHost": "smtp.sendgrid.net", + "smtpPort": 465, + "timeout": 10000, + "enableTls": true, + "tlsVersion": "TLSv1.2", + "authorizationUri": null, + "accessTokenUri": null, + "scope": [ + "" + ], + "helpLink": null, + "name": "SendGrid" + }, + { + "providerId": "GOOGLE", + "smtpProtocol": "SMTPS", + "smtpHost": "smtp.gmail.com", + "smtpPort": 465, + "timeout": 10000, + "enableTls": true, + "tlsVersion": "TLSv1.2", + "authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline", + "accessTokenUri": "https://oauth2.googleapis.com/token", + "scope": [ + "https://mail.google.com/" + ], + "helpLink": "https://support.google.com/googleapi/answer/6158849", + "name": "Google" + }, + { + "providerId": "OFFICE_365", + "smtpProtocol": "SMTP", + "smtpHost": "smtp.office365.com", + "smtpPort": 587, + "timeout": 10000, + "enableTls": true, + "tlsVersion": "TLSv1.2", + "authorizationUri": "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize", + "accessTokenUri": "https://login.microsoftonline.com/%s/oauth2/v2.0/token", + "scope": [ + "https://outlook.office365.com/SMTP.Send", + "offline_access", + "openid" + ], + "helpLink": "https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth", + "name": "Office 365" + } +] \ No newline at end of file diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 46418bb942..8e9c6147f0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -135,6 +135,10 @@ security: path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}" password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}" +mail: + oauth2: + refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}" # Number of seconds (1 day). + # Usage statistics parameters usage: stats: @@ -159,7 +163,7 @@ ui: # Help parameters help: # Base url for UI help assets - base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.5}" + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.5.1}" database: ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records @@ -502,7 +506,7 @@ cache: spring.data.redis.repositories.enabled: false redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -522,6 +526,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" @@ -1261,6 +1275,11 @@ vc: notification_system: thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}" + rules: + trigger_types_configs: + NEW_PLATFORM_VERSION: + # In milliseconds, infinitely by default + deduplication_duration: "${NEW_PLATFORM_VERSION_NOTIFICATION_RULE_DEDUPLICATION_DURATION:0}" management: endpoints: diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 6c42c7604d..d5eabdff2a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -264,7 +264,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { int cntTime = 1; testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionType, cntTime); testLogEntityAction(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); - tesPushMsgToCoreTime(cntTime); + testPushMsgToCoreTime(cntTime); Mockito.reset(tbClusterService, auditLogService); } @@ -363,13 +363,13 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.any(entityId.getClass()), Mockito.any(ComponentLifecycleEvent.class)); } - private void tesPushMsgToCoreTime(int cntTime) { + private void testPushMsgToCoreTime(int cntTime) { Mockito.verify(tbClusterService, times(cntTime)).pushMsgToCore(Mockito.any(ToDeviceActorNotificationMsg.class), Mockito.isNull()); } protected void testLogEntityAction(HasName entity, EntityId originatorId, TenantId tenantId, - CustomerId customerId, UserId userId, String userName, - ActionType actionType, int cntTime, Object... additionalInfo) { + CustomerId customerId, UserId userId, String userName, + ActionType actionType, int cntTime, Object... additionalInfo) { ArgumentMatcher matcherEntityEquals = entity == null ? Objects::isNull : argument -> argument.toString().equals(entity.toString()); ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); ArgumentMatcher matcherCustomerId = customerId == null ? @@ -380,10 +380,10 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { actionType, cntTime, extractMatcherAdditionalInfo(additionalInfo)); } - private void testLogEntityActionEntityEqClass(HasName entity, EntityId originatorId, TenantId tenantId, - CustomerId customerId, UserId userId, String userName, - ActionType actionType, int cntTime, Object... additionalInfo) { - ArgumentMatcher matcherEntityEquals = argument -> argument.getClass().equals(entity.getClass()); + protected void testLogEntityActionEntityEqClass(HasName entity, EntityId originatorId, TenantId tenantId, + CustomerId customerId, UserId userId, String userName, + ActionType actionType, int cntTime, Object... additionalInfo) { + ArgumentMatcher matcherEntityEquals = argument -> entity.getClass().isAssignableFrom(argument.getClass()); ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); ArgumentMatcher matcherCustomerId = customerId == null ? argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId); @@ -600,8 +600,8 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { return fieldName + " length must be equal or less than 255"; } - protected String msgErrorNoFound(String entityClassName, String assetIdStr) { - return entityClassName + " with id [" + assetIdStr + "] is not found"; + protected String msgErrorNoFound(String entityClassName, String entityIdStr) { + return entityClassName + " with id [" + entityIdStr + "] is not found"; } private String entityClassToEntityTypeName(HasName entity) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 724e975792..7f57fbbb3f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -166,6 +166,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { private static final String CUSTOMER_USER_PASSWORD = "customer"; protected static final String DIFFERENT_CUSTOMER_USER_EMAIL = "testdifferentcustomer@thingsboard.org"; + + protected static final String DIFFERENT_TENANT_CUSTOMER_USER_EMAIL = "testdifferenttenantcustomer@thingsboard.org"; private static final String DIFFERENT_CUSTOMER_USER_PASSWORD = "diffcustomer"; /** @@ -191,9 +193,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected CustomerId customerId; protected TenantId differentTenantId; protected CustomerId differentCustomerId; + + protected CustomerId differentTenantCustomerId; protected UserId customerUserId; protected UserId differentCustomerUserId; + protected UserId differentTenantCustomerUserId; + @SuppressWarnings("rawtypes") private HttpMessageConverter mappingJackson2HttpMessageConverter; @@ -365,7 +371,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected Tenant savedDifferentTenant; protected User savedDifferentTenantUser; private Customer savedDifferentCustomer; + private Customer savedDifferentTenantCustomer; protected User differentCustomerUser; + protected User differentTenantCustomerUser; protected void loginDifferentTenant() throws Exception { if (savedDifferentTenant != null) { @@ -407,6 +415,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } } + protected void loginDifferentTenantCustomer() throws Exception { + if (savedDifferentTenantCustomer != null) { + login(savedDifferentTenantCustomer.getEmail(), CUSTOMER_USER_PASSWORD); + } else { + createDifferentTenantCustomer(); + + loginDifferentTenant(); + differentTenantCustomerUser = new User(); + differentTenantCustomerUser.setAuthority(Authority.CUSTOMER_USER); + differentTenantCustomerUser.setTenantId(savedDifferentTenantCustomer.getTenantId()); + differentTenantCustomerUser.setCustomerId(savedDifferentTenantCustomer.getId()); + differentTenantCustomerUser.setEmail(DIFFERENT_TENANT_CUSTOMER_USER_EMAIL); + + differentTenantCustomerUser = createUserAndLogin(differentTenantCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD); + differentTenantCustomerUserId = differentTenantCustomerUser.getId(); + } + } + protected void createDifferentCustomer() throws Exception { loginTenantAdmin(); @@ -419,6 +445,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { resetTokens(); } + protected void createDifferentTenantCustomer() throws Exception { + loginDifferentTenant(); + + Customer customer = new Customer(); + customer.setTitle("Different tenant customer"); + savedDifferentTenantCustomer = doPost("/api/customer", customer, Customer.class); + Assert.assertNotNull(savedDifferentTenantCustomer); + differentTenantCustomerId = savedDifferentTenantCustomer.getId(); + + resetTokens(); + } + protected void deleteDifferentTenant() throws Exception { if (savedDifferentTenant != null) { loginSysAdmin(); @@ -601,6 +639,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(getRequest); } + protected ResultActions doGet(String urlTemplate, HttpHeaders httpHeaders, Object... urlVariables) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); + getRequest.headers(httpHeaders); + setJwtToken(getRequest); + return mockMvc.perform(getRequest); + } + protected T doGet(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java index 287ae7cfe1..bf3e7528c9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java @@ -46,6 +46,7 @@ import java.util.LinkedList; import java.util.List; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j @@ -104,7 +105,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { AlarmComment createdComment = createAlarmComment(alarm.getId()); - testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); + testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); } @Test @@ -116,7 +117,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { AlarmComment createdComment = createAlarmComment(alarm.getId()); Assert.assertEquals(AlarmCommentType.OTHER, createdComment.getType()); - testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); + testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); } @Test @@ -135,7 +136,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText()); Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn")); - testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment); + testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment); } @Test @@ -154,7 +155,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText()); Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn")); - testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment); + testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment); } @Test @@ -169,8 +170,8 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { savedComment.setComment(newComment); doPost("/api/alarm/" + alarm.getId() + "/comment", savedComment) - .andExpect(status().isForbidden()) - .andExpect(statusReason(containsString(msgErrorPermission))); + .andExpect(status().isNotFound()) + .andExpect(statusReason(equalTo(msgErrorNoFound("Alarm", alarm.getId().toString())))); testNotifyEntityNever(alarm.getId(), savedComment); } @@ -209,7 +210,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { .comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment", CUSTOMER_USER_EMAIL))) .build(); - testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment); + testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment); } @Test @@ -228,7 +229,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { .comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment", TENANT_ADMIN_EMAIL))) .build(); - testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment); + testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment); } @Test @@ -355,16 +356,18 @@ public class AlarmCommentControllerTest extends AbstractControllerTest { Assert.assertTrue("Created alarm doesn't match the found one!", equals); } - private AlarmComment createAlarmComment(AlarmId alarmId, String text) { + private AlarmComment createAlarmComment(AlarmId alarmId, String text) { AlarmComment alarmComment = AlarmComment.builder() .comment(JacksonUtil.newObjectNode().set("text", new TextNode(text))) .build(); return saveAlarmComment(alarmId, alarmComment); } - private AlarmComment createAlarmComment(AlarmId alarmId) { + + private AlarmComment createAlarmComment(AlarmId alarmId) { return createAlarmComment(alarmId, "Please take a look"); } + private AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) { alarmComment = doPost("/api/alarm/" + alarmId + "/comment", alarmComment, AlarmComment.class); Assert.assertNotNull(alarmComment); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java new file mode 100644 index 0000000000..465ec37bb9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java @@ -0,0 +1,96 @@ +/** + * 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. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class BaseQueueControllerTest extends AbstractControllerTest { + + @Test + public void testQueueWithServiceTypeRE() throws Exception { + loginSysAdmin(); + + // create queue + Queue queue = new Queue(); + queue.setName("qwerty"); + queue.setTopic("tb_rule_engine.qwerty"); + queue.setPollInterval(25); + queue.setPartitions(10); + queue.setTenantId(TenantId.SYS_TENANT_ID); + queue.setConsumerPerPartition(false); + queue.setPackProcessingTimeout(2000); + SubmitStrategy submitStrategy = new SubmitStrategy(); + submitStrategy.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR); + queue.setSubmitStrategy(submitStrategy); + ProcessingStrategy processingStrategy = new ProcessingStrategy(); + processingStrategy.setType(ProcessingStrategyType.RETRY_ALL); + processingStrategy.setRetries(3); + processingStrategy.setFailurePercentage(0.7); + processingStrategy.setPauseBetweenRetries(3); + processingStrategy.setMaxPauseBetweenRetries(5); + queue.setProcessingStrategy(processingStrategy); + + // create queue + Queue queue2 = new Queue(); + queue2.setName("qwerty2"); + queue2.setTopic("tb_rule_engine.qwerty2"); + queue2.setPollInterval(25); + queue2.setPartitions(10); + queue2.setTenantId(TenantId.SYS_TENANT_ID); + queue2.setConsumerPerPartition(false); + queue2.setPackProcessingTimeout(2000); + submitStrategy.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR); + queue2.setSubmitStrategy(submitStrategy); + processingStrategy.setType(ProcessingStrategyType.RETRY_ALL); + processingStrategy.setRetries(3); + processingStrategy.setFailurePercentage(0.7); + processingStrategy.setPauseBetweenRetries(3); + processingStrategy.setMaxPauseBetweenRetries(5); + queue2.setProcessingStrategy(processingStrategy); + + Queue savedQueue = doPost("/api/queues?serviceType=" + "TB-RULE-ENGINE", queue, Queue.class); + Queue savedQueue2 = doPost("/api/queues?serviceType=" + "TB_RULE_ENGINE", queue2, Queue.class); + + PageLink pageLink = new PageLink(10); + PageData pageData; + pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB-RULE-ENGINE&", new TypeReference<>() { + }, pageLink); + Assert.assertFalse(pageData.getData().isEmpty()); + doDelete("/api/queues/" + savedQueue.getUuidId()) + .andExpect(status().isOk()); + + pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference<>() { + }, pageLink); + Assert.assertFalse(pageData.getData().isEmpty()); + doDelete("/api/queues/" + savedQueue2.getUuidId()) + .andExpect(status().isOk()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index de8f717c0d..b4735519a5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -16,11 +16,18 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.ResultActions; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; @@ -35,6 +42,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.List; @@ -47,6 +55,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { private IdComparator idComparator = new IdComparator<>(); private static final String DEFAULT_FILE_NAME = "test.jks"; + private static final String DEFAULT_FILE_NAME_2 = "test2.jks"; + private static final String JS_TEST_FILE_NAME = "test.js"; + private static final String TEST_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; + private Tenant savedTenant; private User tenantAdmin; @@ -87,7 +99,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = save(resource); @@ -122,7 +134,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle(StringUtils.randomAlphabetic(300)); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); Mockito.reset(tbClusterService, auditLogService); @@ -141,7 +153,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = save(resource); @@ -170,7 +182,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = save(resource); @@ -185,7 +197,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = save(resource); @@ -216,7 +228,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); resources.add(new TbResourceInfo(save(resource))); } List loadedResources = new ArrayList<>(); @@ -242,6 +254,54 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertEquals(resources, loadedResources); } + @Test + public void testFindTenantTbResourcesByType() throws Exception { + Mockito.reset(tbClusterService, auditLogService); + + List resources = new ArrayList<>(); + int jksCntEntity = 17; + for (int i = 0; i < jksCntEntity; i++) { + TbResource resource = new TbResource(); + resource.setTitle("JKS Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData(TEST_DATA); + resources.add(new TbResourceInfo(save(resource))); + } + + int lwm2mCntEntity = 19; + for (int i = 0; i < lwm2mCntEntity; i++) { + TbResource resource = new TbResource(); + resource.setTitle("LWM2M Resource" + i); + resource.setResourceType(ResourceType.PKCS_12); + resource.setFileName(i + DEFAULT_FILE_NAME_2); + resource.setData(TEST_DATA); + save(resource); + } + + List loadedResources = new ArrayList<>(); + PageLink pageLink = new PageLink(5); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS.name() + "&", + new TypeReference<>() { + }, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new TbResource(), new TbResource(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.ADDED, jksCntEntity + lwm2mCntEntity); + + Collections.sort(resources, idComparator); + Collections.sort(loadedResources, idComparator); + + Assert.assertEquals(resources, loadedResources); + } + @Test public void testFindSystemTbResources() throws Exception { loginSysAdmin(); @@ -252,7 +312,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); resources.add(new TbResourceInfo(save(resource))); } List loadedResources = new ArrayList<>(); @@ -300,6 +360,86 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertTrue(loadedResources.isEmpty()); } + @Test + public void testFindSystemTbResourcesByType() throws Exception { + loginSysAdmin(); + + List jksResources = new ArrayList<>(); + List lwm2mesources = new ArrayList<>(); + int jksCntEntity = 17; + for (int i = 0; i < jksCntEntity; i++) { + TbResource resource = new TbResource(); + resource.setTitle("JKS Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData(TEST_DATA); + TbResourceInfo saved = new TbResourceInfo(save(resource)); + jksResources.add(saved); + } + + int lwm2mCntEntity = 19; + for (int i = 0; i < lwm2mCntEntity; i++) { + TbResource resource = new TbResource(); + resource.setTitle("LWM2M Resource" + i); + resource.setResourceType(ResourceType.PKCS_12); + resource.setFileName(i + DEFAULT_FILE_NAME_2); + resource.setData(TEST_DATA); + TbResource saved = save(resource); + lwm2mesources.add(saved); + } + + List loadedResources = new ArrayList<>(); + PageLink pageLink = new PageLink(30); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS + "&", + new TypeReference<>() { + }, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(jksResources, idComparator); + Collections.sort(loadedResources, idComparator); + + Assert.assertEquals(jksResources, loadedResources); + + Mockito.reset(tbClusterService, auditLogService); + + int cntEntity = jksResources.size(); + for (TbResourceInfo resource : jksResources) { + doDelete("/api/resource/" + resource.getId().getId().toString()) + .andExpect(status().isOk()); + } + + testNotifyManyEntityManyTimeMsgToEdgeServiceNeverAdditionalInfoAny(new TbResource(), new TbResource(), + jksResources.get(0).getTenantId(), null, null, SYS_ADMIN_EMAIL, + ActionType.DELETED, cntEntity, 1); + + pageLink = new PageLink(27); + loadedResources.clear(); + do { + pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS + "&", + new TypeReference<>() { + }, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertTrue(loadedResources.isEmpty()); + + loginSysAdmin(); + + for (TbResourceInfo resource : lwm2mesources) { + doDelete("/api/resource/" + resource.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + @Test public void testFindSystemAndTenantTbResources() throws Exception { List systemResources = new ArrayList<>(); @@ -309,7 +449,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); expectedResources.add(new TbResourceInfo(save(resource))); } @@ -320,7 +460,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResourceInfo savedResource = new TbResourceInfo(save(resource)); systemResources.add(savedResource); if (i >= 73) { @@ -356,6 +496,99 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } + @Test + public void testDownloadTbResourceIfChanged() throws Exception { + Mockito.reset(tbClusterService, auditLogService); + + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("Js resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setData(TEST_DATA); + + TbResource savedResource = save(resource); + + testNotifyEntityOneTimeMsgToEdgeServiceNever(savedResource, savedResource.getId(), savedResource.getId(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.ADDED); + + ResultActions resultActions = doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download") + .andExpect(status().isOk()); + MockHttpServletResponse response = resultActions.andReturn().getResponse(); + String eTag = response.getHeader("ETag"); + Assert.assertNotNull(eTag); + Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA); + + //download with if-none-match header + HttpHeaders headers = new HttpHeaders(); + headers.setIfNoneMatch(eTag); + doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download", headers) + .andExpect(status().isNotModified()); + } + + @Test + public void testDownloadTbResourceIfChangedAsPublicCustomer() throws Exception { + loginTenantAdmin(); + Mockito.reset(tbClusterService, auditLogService); + + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("Js resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setData(TEST_DATA); + + TbResource savedResource = save(resource); + + //download as public customer + Device device = new Device(); + device.setName("Test Public Device"); + device.setLabel("Label"); + device.setCustomerId(customerId); + device = doPost("/api/device", device, Device.class); + device = doPost("/api/customer/public/device/" + device.getUuidId(), Device.class); + + String publicId = device.getCustomerId().toString(); + + Mockito.reset(tbClusterService, auditLogService); + resetTokens(); + + JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}"); + JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class); + this.token = tokens.get("token").asText(); + + ResultActions resultActions = doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download") + .andExpect(status().isOk()); + MockHttpServletResponse response = resultActions.andReturn().getResponse(); + String eTag = response.getHeader("ETag"); + Assert.assertNotNull(eTag); + Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA); + + //download with if-none-match header + HttpHeaders headers = new HttpHeaders(); + headers.setIfNoneMatch(eTag); + doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download", headers) + .andExpect(status().isNotModified()); + } + + @Test + public void testDownloadTbResourceIfChangedAsCustomerOfDifferentTenant() throws Exception { + loginTenantAdmin(); + Mockito.reset(tbClusterService, auditLogService); + + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("Js resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setData(TEST_DATA); + + TbResource savedResource = save(resource); + + loginDifferentTenant(); + loginDifferentTenantCustomer(); + doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download") + .andExpect(status().isForbidden()); + } + private TbResource save(TbResource tbResource) throws Exception { return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>(){}); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index ac3857dbdf..f45882e416 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.util.Pair; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; @@ -43,6 +45,7 @@ import org.thingsboard.server.common.data.notification.settings.NotificationSett import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -54,6 +57,10 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import java.net.URISyntaxException; import java.util.Arrays; @@ -72,21 +79,40 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest @MockBean protected SlackService slackService; - @Autowired protected MailService mailService; + @Autowired + protected NotificationRuleService notificationRuleService; + @Autowired + protected NotificationTemplateService notificationTemplateService; + @Autowired + protected NotificationTargetService notificationTargetService; + @Autowired + protected NotificationRequestService notificationRequestService; + public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; + @After + public void afterEach() { + notificationRequestService.deleteNotificationRequestsByTenantId(TenantId.SYS_TENANT_ID); + notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID); + notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID); + notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID); + } + protected NotificationTarget createNotificationTarget(UserId... usersIds) { - NotificationTarget notificationTarget = new NotificationTarget(); - notificationTarget.setTenantId(tenantId); - notificationTarget.setName("Users " + List.of(usersIds)); - PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); UserListFilter filter = new UserListFilter(); filter.setUsersIds(DaoUtil.toUUIDs(List.of(usersIds))); - targetConfig.setUsersFilter(filter); + return createNotificationTarget(filter); + } + + protected NotificationTarget createNotificationTarget(UsersFilter usersFilter) { + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setName(usersFilter.toString()); + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + targetConfig.setUsersFilter(usersFilter); notificationTarget.setConfiguration(targetConfig); return saveNotificationTarget(notificationTarget); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index 455b897fcd..6c4413d1e7 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -17,12 +17,14 @@ package org.thingsboard.server.service.notification; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.util.Pair; +import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -31,10 +33,15 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; @@ -42,6 +49,8 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; @@ -52,8 +61,11 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig; @@ -68,13 +80,16 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger; +import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.util.limits.LimitedApi; import org.thingsboard.server.dao.util.limits.RateLimitService; -import org.thingsboard.server.queue.notification.NotificationRuleProcessor; +import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache; +import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.ArrayList; @@ -94,8 +109,15 @@ import static org.assertj.core.api.Assertions.offset; import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig.Action.ASSIGNED; +import static org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig.Action.UNASSIGNED; +import static org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent.ACTIVE; +import static org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent.INACTIVE; @DaoSqlTest +@TestPropertySource(properties = { + "transport.http.enabled=true" +}) public class NotificationRuleApiTest extends AbstractNotificationApiTest { @SpyBean @@ -108,6 +130,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { private RuleChainService ruleChainService; @Autowired private NotificationRuleProcessor notificationRuleProcessor; + @Autowired + private DefaultNotifications defaultNotifications; + @Autowired + private DefaultNotificationRulesCache notificationRulesCache; + @Autowired + private DeviceStateService deviceStateService; @Before public void beforeEach() throws Exception { @@ -297,7 +325,9 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notification = getWsClient().getLastDataUpdate().getUpdate(); assertThat(notification.getSubject()).isEqualTo("critical alarm '" + alarmType + "' is CLEARED_UNACK"); - assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); + }); } @Test @@ -370,6 +400,122 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { }); } + @Test + public void testNotificationRuleProcessing_alarmAssignment() throws Exception { + AlarmAssignmentNotificationRuleTriggerConfig triggerConfig = AlarmAssignmentNotificationRuleTriggerConfig.builder() + .alarmTypes(Set.of("test")) + .notifyOn(Set.of(ASSIGNED, UNASSIGNED)) + .build(); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}'. Assignee: ${assigneeEmail}"; + createNotificationRule(triggerConfig, "Test", template, target.getId()); + + Device device = createDevice("Device A", "123"); + Alarm alarm = Alarm.builder() + .tenantId(tenantId) + .originator(device.getId()) + .cleared(false) + .acknowledged(false) + .severity(AlarmSeverity.CRITICAL) + .type("test") + .startTs(System.currentTimeMillis()) + .build(); + alarm = doPost("/api/alarm", alarm, Alarm.class); + AlarmId alarmId = alarm.getId(); + + checkNotificationAfter(() -> { + doPost("/api/alarm/" + alarmId + "/assign/" + tenantAdminUserId).andExpect(status().isOk()); + }, notification -> { + assertThat(notification.getText()).isEqualTo( + TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL + ); + }); + + checkNotificationAfter(() -> { + doDelete("/api/alarm/" + alarmId + "/assign").andExpect(status().isOk()); + }, notification -> { + assertThat(notification.getText()).isEqualTo( + TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A'. Assignee: " + ); + }); + } + + @Test + public void testNotificationRuleProcessing_alarmComment() throws Exception { + AlarmCommentNotificationRuleTriggerConfig triggerConfig = AlarmCommentNotificationRuleTriggerConfig.builder() + .alarmTypes(Set.of("test")) + .onlyUserComments(true) + .notifyOnCommentUpdate(true) + .build(); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + String template = "${userEmail} ${action} comment on alarm ${alarmType}: ${comment}"; + createNotificationRule(triggerConfig, "Test", template, target.getId()); + + Device device = createDevice("Device A", "123"); + Alarm alarm = Alarm.builder() + .tenantId(tenantId) + .originator(device.getId()) + .cleared(false) + .acknowledged(false) + .severity(AlarmSeverity.CRITICAL) + .type("test") + .startTs(System.currentTimeMillis()) + .build(); + alarm = doPost("/api/alarm", alarm, Alarm.class); + AlarmId alarmId = alarm.getId(); + + AlarmComment comment = checkNotificationAfter(() -> { + return doPost("/api/alarm/" + alarmId + "/comment", + AlarmComment.builder() + .type(AlarmCommentType.OTHER) + .comment(JacksonUtil.newObjectNode() + .put("text", "this is bad")) + .build(), AlarmComment.class); + }, (notification, r) -> { + assertThat(notification.getText()).isEqualTo( + TENANT_ADMIN_EMAIL + " added comment on alarm test: this is bad" + ); + }); + + checkNotificationAfter(() -> { + ((ObjectNode) comment.getComment()).put("text", "this is very bad"); + doPost("/api/alarm/" + alarmId + "/comment", comment); + }, notification -> { + assertThat(notification.getText()).isEqualTo( + TENANT_ADMIN_EMAIL + " updated comment on alarm test: this is very bad" + ); + }); + } + + @Test + public void testNotificationRuleProcessing_deviceActivity() throws Exception { + DeviceActivityNotificationRuleTriggerConfig triggerConfig = DeviceActivityNotificationRuleTriggerConfig.builder() + .notifyOn(Set.of(ACTIVE, INACTIVE)) + .build(); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + String template = "Device ${deviceName} (${deviceLabel}) of type ${deviceType} is now ${eventType}"; + createNotificationRule(triggerConfig, "Test", template, target.getId()); + + Device device = new Device(); + device.setName("A"); + device.setLabel("Test Device A"); + device.setType("test"); + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + device.setDeviceData(deviceData); + device = doPost("/api/device", device, Device.class); + DeviceId deviceId = device.getId(); + + checkNotificationAfter(() -> { + deviceStateService.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + }, notification -> { + assertThat(notification.getText()).isEqualTo( + "Device A (Test Device A) of type test is now active" + ); + }); + } + @Test public void testNotificationRuleInfo() throws Exception { NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL}; @@ -444,7 +590,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { } @Test - public void testNotificationsDeduplication() throws Exception { + public void testNotificationsDeduplication_newPlatformVersion() throws Exception { loginSysAdmin(); NewPlatformVersionNotificationRuleTriggerConfig triggerConfig = new NewPlatformVersionNotificationRuleTriggerConfig(); createNotificationRule(triggerConfig, "Test", "Test", createNotificationTarget(tenantAdminUserId).getId()); @@ -477,6 +623,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE)); triggerConfig.setCreated(true); NotificationRule rule = createNotificationRule(triggerConfig, "Created", "Created", createNotificationTarget(tenantAdminUserId).getId()); + notificationRulesCache.evict(tenantId); assertThat(getMyNotifications(false, 100)).size().isZero(); createDevice("Device 1", "default", "111"); @@ -487,6 +634,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { rule.setEnabled(false); saveNotificationRule(rule); + notificationRulesCache.evict(tenantId); createDevice("Device 2", "default", "222"); TimeUnit.SECONDS.sleep(5); @@ -494,7 +642,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { rule.setEnabled(true); saveNotificationRule(rule); - TimeUnit.SECONDS.sleep(2); // for rule update event to reach rules cache + notificationRulesCache.evict(tenantId); createDevice("Device 3", "default", "333"); await().atMost(30, TimeUnit.SECONDS) diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index 5c9d94cde2..6f08e47412 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; @@ -104,6 +105,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { ""; private static final String DEFAULT_FILE_NAME = "test.jks"; + private static final String TEST_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; private IdComparator idComparator = new IdComparator<>(); @@ -146,7 +148,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { @Test public void testSaveResourceWithMaxSumDataSizeOutOfLimit() throws Exception { loginSysAdmin(); - long limit = 1; + long limit = 4; EntityInfo defaultTenantProfileInfo = doGet("/api/tenantProfileInfo/default", EntityInfo.class); TenantProfile defaultTenantProfile = doGet("/api/tenantProfile/" + defaultTenantProfileInfo.getId().getId().toString(), TenantProfile.class); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(limit).build()); @@ -158,7 +160,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { createResource("test", DEFAULT_FILE_NAME); - assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(4, resourceService.sumDataSizeByTenantId(tenantId)); try { assertThatThrownBy(() -> createResource("test1", 1 + DEFAULT_FILE_NAME)) @@ -176,16 +178,12 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId)); createResource("test", DEFAULT_FILE_NAME); - assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(4, resourceService.sumDataSizeByTenantId(tenantId)); - int maxSumDataSize = 8; - - for (int i = 2; i <= maxSumDataSize; i++) { + for (int i = 2; i < 4; i++) { createResource("test" + i, i + DEFAULT_FILE_NAME); - assertEquals(i, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(i*4, resourceService.sumDataSizeByTenantId(tenantId)); } - - assertEquals(maxSumDataSize, resourceService.sumDataSizeByTenantId(tenantId)); } private TbResource createResource(String title, String filename) throws Exception { @@ -194,7 +192,8 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle(title); resource.setResourceType(ResourceType.JKS); resource.setFileName(filename); - resource.setData("1"); + byte[] b = new byte[1]; + resource.setData(Base64.getEncoder().encodeToString(b)); return resourceService.save(resource); } @@ -205,7 +204,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = resourceService.save(resource); @@ -253,7 +252,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = resourceService.save(resource); assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId()); @@ -268,7 +267,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = resourceService.save(resource); @@ -277,7 +276,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); try { Assertions.assertThrows(DataValidationException.class, () -> { @@ -294,7 +293,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTenantId(tenantId); resource.setResourceType(ResourceType.JKS); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); Assertions.assertThrows(DataValidationException.class, () -> { resourceService.save(resource); }); @@ -307,7 +306,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); Assertions.assertThrows(DataValidationException.class, () -> { resourceService.save(resource); }); @@ -334,7 +333,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = resourceService.save(resource); TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); @@ -350,7 +349,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTenantId(tenantId); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = resourceService.save(resource); TbResource foundResource = resourceService.getResource(tenantId, savedResource.getResourceType(), savedResource.getResourceKey()); @@ -365,7 +364,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResource savedResource = resourceService.save(resource); TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); @@ -391,7 +390,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); resources.add(new TbResourceInfo(resourceService.save(resource))); } @@ -399,7 +398,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { PageLink pageLink = new PageLink(16); PageData pageData; do { - pageData = resourceService.findTenantResourcesByTenantId(tenantId, pageLink); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(tenantId) + .build(); + pageData = resourceService.findTenantResourcesByTenantId(filter, pageLink); loadedResources.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -414,7 +416,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resourceService.deleteResourcesByTenantId(tenantId); pageLink = new PageLink(31); - pageData = resourceService.findTenantResourcesByTenantId(tenantId, pageLink); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(tenantId) + .build(); + pageData = resourceService.findTenantResourcesByTenantId(filter, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -439,7 +444,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("System Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); TbResourceInfo tbResourceInfo = new TbResourceInfo(resourceService.save(resource)); if (i >= 50) { resources.add(tbResourceInfo); @@ -452,7 +457,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("Tenant Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData("Test Data"); + resource.setData(TEST_DATA); resources.add(new TbResourceInfo(resourceService.save(resource))); } @@ -460,7 +465,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { PageLink pageLink = new PageLink(10); PageData pageData; do { - pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(tenantId) + .build(); + pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink); loadedResources.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -475,14 +483,20 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resourceService.deleteResourcesByTenantId(tenantId); pageLink = new PageLink(100); - pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(tenantId) + .build(); + pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink); Assert.assertFalse(pageData.hasNext()); assertEquals(pageData.getData().size(), 100); resourceService.deleteResourcesByTenantId(TenantId.SYS_TENANT_ID); pageLink = new PageLink(100); - pageData = resourceService.findAllTenantResourcesByTenantId(TenantId.SYS_TENANT_ID, pageLink); + filter = TbResourceInfoFilter.builder() + .tenantId(TenantId.SYS_TENANT_ID) + .build(); + pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtilsTest.java new file mode 100644 index 0000000000..7ba760a45d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtilsTest.java @@ -0,0 +1,58 @@ +/** + * 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. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME; + +public class CookieUtilsTest { + + @Test + public void serializeDeserializeOAuth2AuthorizationRequestTest() { + HttpCookieOAuth2AuthorizationRequestRepository cookieRequestRepo = new HttpCookieOAuth2AuthorizationRequestRepository(); + HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class); + + Map additionalParameters = new LinkedHashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + var request = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri("testUri").clientId("testId") + .scope("read", "write") + .additionalParameters(additionalParameters).build(); + + + Cookie cookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(request)); + Mockito.when(servletRequest.getCookies()).thenReturn(new Cookie[]{cookie}); + + OAuth2AuthorizationRequest deserializedRequest = cookieRequestRepo.loadAuthorizationRequest(servletRequest); + + assertNotNull(deserializedRequest); + assertEquals(request.getGrantType(), deserializedRequest.getGrantType()); + assertEquals(request.getAuthorizationUri(), deserializedRequest.getAuthorizationUri()); + assertEquals(request.getClientId(), deserializedRequest.getClientId()); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java deleted file mode 100644 index 3691952b22..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.service.security.auth.oauth2; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.Serializable; - -import static org.junit.Assert.assertEquals; -import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME; - -public class HttpCookieOAuth2AuthorizationRequestRepositoryTest { - - private static final String SERIALIZED_ATTACK_STRING = - "rO0ABXNyAHVvcmcudGhpbmdzYm9hcmQuc2VydmVyLnNlcnZpY2Uuc2VjdXJpdHkuYXV0aC5vYXV0aDIuSHR0cENvb2tpZU9BdXRoMkF1dGhvcml6YXRpb25SZXF1ZXN0UmVwb3NpdG9yeVRlc3QkTWFsaWNpb3VzQ2xhc3MAAAAAAAAAAAIAAHhw"; - - private static int maliciousMethodInvocationCounter; - - @Before - public void resetInvocationCounter() { - maliciousMethodInvocationCounter = 0; - } - - @Test - public void whenLoadAuthorizationRequest_thenMaliciousMethodNotInvoked() { - HttpCookieOAuth2AuthorizationRequestRepository cookieRequestRepo = new HttpCookieOAuth2AuthorizationRequestRepository(); - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Cookie cookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, SERIALIZED_ATTACK_STRING); - Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); - - cookieRequestRepo.loadAuthorizationRequest(request); - - assertEquals(0, maliciousMethodInvocationCounter); - } - - private static class MaliciousClass implements Serializable { - private static final long serialVersionUID = 0L; - - public void maliciousMethod() { - maliciousMethodInvocationCounter++; - } - - private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { - maliciousMethod(); - } - } -} diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 8245284af6..52f2ec5c9d 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -29,12 +29,11 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import java.util.Map; import java.util.UUID; @@ -42,6 +41,7 @@ import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -69,7 +69,7 @@ public class DefaultDeviceStateServiceTest { @Before public void setUp() { - service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null)); + service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null, mock(NotificationRuleProcessor.class))); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java index 2dda86d7a4..07eadd731d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java @@ -21,25 +21,14 @@ import org.eclipse.californium.core.CoapHandler; import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.coap.CoAP; -import java.util.concurrent.CountDownLatch; - @Slf4j @Data public class CoapTestCallback implements CoapHandler { - protected final CountDownLatch latch; protected Integer observe; protected byte[] payloadBytes; protected CoAP.ResponseCode responseCode; - public CoapTestCallback() { - this.latch = new CountDownLatch(1); - } - - public CoapTestCallback(int subscribeCount) { - this.latch = new CountDownLatch(subscribeCount); - } - public Integer getObserve() { return observe; } @@ -57,7 +46,6 @@ public class CoapTestCallback implements CoapHandler { observe = response.getOptions().getObserve(); payloadBytes = response.getPayload(); responseCode = response.getCode(); - latch.countDown(); } @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java index 697779c178..03ef6f9503 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java @@ -232,7 +232,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap } client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES); - CoapTestCallback callbackCoap = new CoapTestCallback(1); + CoapTestCallback callbackCoap = new CoapTestCallback(); CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap); String awaitAlias = "await Json Test Subscribe To AttributesUpdates (client.getObserveRelation)"; @@ -279,7 +279,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap } client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES); - CoapTestCallback callbackCoap = new CoapTestCallback(1); + CoapTestCallback callbackCoap = new CoapTestCallback(); String awaitAlias = "await Proto Test Subscribe To Attributes Updates (add attributes)"; CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java index a2b44b6e58..445ac51ccd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java @@ -41,7 +41,6 @@ import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest; import org.thingsboard.server.transport.coap.CoapTestCallback; import org.thingsboard.server.transport.coap.CoapTestClient; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; @@ -74,17 +73,16 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC protected void processOneWayRpcTest(boolean protobuf) throws Exception { client = new CoapTestClient(accessToken, FeatureType.RPC); - CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, 1, true, protobuf); + CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, true, protobuf); CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap); String awaitAlias = "await One Way Rpc (client.getObserveRelation)"; await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && - callbackCoap.getObserve() != null && - 0 == callbackCoap.getObserve().intValue()); + callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve()); validateCurrentStateNotification(callbackCoap); - int expectedObserveCountAfterGpioRequest = callbackCoap.getObserve().intValue() + 1; + int expectedObserveAfterRpcProcessed = callbackCoap.getObserve() + 1; String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); @@ -92,8 +90,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && - callbackCoap.getObserve() != null && - expectedObserveCountAfterGpioRequest == callbackCoap.getObserve().intValue()); + callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed == callbackCoap.getObserve()); validateOneWayStateChangedNotification(callbackCoap, result); observeRelation.proactiveCancel(); @@ -102,7 +99,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC protected void processTwoWayRpcTest(String expectedResponseResult, boolean protobuf) throws Exception { client = new CoapTestClient(accessToken, FeatureType.RPC); - CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, 1, false, protobuf); + CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, false, protobuf); CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap); String awaitAlias = "await Two Way Rpc (client.getObserveRelation)"; @@ -110,29 +107,29 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && - 0 == callbackCoap.getObserve().intValue()); + 0 == callbackCoap.getObserve()); validateCurrentStateNotification(callbackCoap); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); - int expectedObserveCountAfterGpioRequest1 = callbackCoap.getObserve().intValue() + 1; + int expectedObserveCountAfterGpioRequest1 = callbackCoap.getObserve() + 1; String actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); awaitAlias = "await Two Way Rpc (setGpio(method, params, value) first"; await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && - expectedObserveCountAfterGpioRequest1 == callbackCoap.getObserve().intValue()); + expectedObserveCountAfterGpioRequest1 == callbackCoap.getObserve()); validateTwoWayStateChangedNotification(callbackCoap, expectedResponseResult, actualResult); - int expectedObserveCountAfterGpioRequest2 = callbackCoap.getObserve().intValue() + 1; + int expectedObserveCountAfterGpioRequest2 = callbackCoap.getObserve() + 1; actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); awaitAlias = "await Two Way Rpc (setGpio(method, params, value) first"; await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && - expectedObserveCountAfterGpioRequest2 == callbackCoap.getObserve().intValue()); + expectedObserveCountAfterGpioRequest2 == callbackCoap.getObserve()); validateTwoWayStateChangedNotification(callbackCoap, expectedResponseResult, actualResult); @@ -140,24 +137,24 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC assertTrue(observeRelation.isCanceled()); } - protected void processOnLoadResponse(CoapResponse response, CoapTestClient client, Integer observe, CountDownLatch latch) { + protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) { JsonNode responseJson = JacksonUtil.fromBytes(response.getPayload()); - client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, responseJson.get("id").asInt())); + int requestId = responseJson.get("id").asInt(); + client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, requestId)); client.postMethod(new CoapHandler() { @Override public void onLoad(CoapResponse response) { - log.warn("Command Response Ack: {}, {}", response.getCode(), response.getResponseText()); - latch.countDown(); + log.warn("RPC {} command response ack: {}", requestId, response.getCode()); } @Override public void onError() { - log.warn("Command Response Ack Error, No connect"); + log.warn("RPC {} command response ack error, no connect", requestId); } }, DEVICE_RESPONSE, MediaTypeRegistry.APPLICATION_JSON); } - protected void processOnLoadProtoResponse(CoapResponse response, CoapTestClient client, Integer observe, CountDownLatch latch) { + protected void processOnLoadProtoResponse(CoapResponse response, CoapTestClient client) { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration(); ProtoFileElement rpcRequestProtoFileElement = DynamicProtoUtils.getProtoFileElement(protoTransportPayloadConfiguration.getDeviceRpcRequestProtoSchema()); DynamicSchema rpcRequestProtoSchema = DynamicProtoUtils.getDynamicSchema(rpcRequestProtoFileElement, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA); @@ -180,13 +177,12 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC client.postMethod(new CoapHandler() { @Override public void onLoad(CoapResponse response) { - log.warn("Command Response Ack: {}", response.getCode()); - latch.countDown(); + log.warn("RPC {} command response ack: {}", requestId, response.getCode()); } @Override public void onError() { - log.warn("Command Response Ack Error, No connect"); + log.warn("RPC {} command response ack error, no connect", requestId); } }, rpcResponseMsg.toByteArray(), MediaTypeRegistry.APPLICATION_JSON); } catch (InvalidProtocolBufferException e) { @@ -226,8 +222,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC private final boolean isOneWayRpc; private final boolean protobuf; - TestCoapCallbackForRPC(CoapTestClient client, int subscribeCount, boolean isOneWayRpc, boolean protobuf) { - super(subscribeCount); + TestCoapCallbackForRPC(CoapTestClient client, boolean isOneWayRpc, boolean protobuf) { this.client = client; this.isOneWayRpc = isOneWayRpc; this.protobuf = protobuf; @@ -241,12 +236,10 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC if (observe != null) { if (!isOneWayRpc && observe > 0) { if (!protobuf){ - processOnLoadResponse(response, client, observe, latch); + processOnLoadResponse(response, client); } else { - processOnLoadProtoResponse(response, client, observe, latch); + processOnLoadProtoResponse(response, client); } - } else { - latch.countDown(); } } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java index 3240647956..208189ac4e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java @@ -32,7 +32,6 @@ public class MqttTestCallback implements MqttCallback { protected final CountDownLatch deliveryLatch; protected int qoS; protected byte[] payloadBytes; - protected String awaitSubTopic; protected boolean pubAckReceived; public MqttTestCallback() { @@ -45,12 +44,6 @@ public class MqttTestCallback implements MqttCallback { this.deliveryLatch = new CountDownLatch(1); } - public MqttTestCallback(String awaitSubTopic) { - this.subscribeLatch = new CountDownLatch(1); - this.deliveryLatch = new CountDownLatch(1); - this.awaitSubTopic = awaitSubTopic; - } - @Override public void connectionLost(Throwable throwable) { log.warn("connectionLost: ", throwable); @@ -59,23 +52,10 @@ public class MqttTestCallback implements MqttCallback { @Override public void messageArrived(String requestTopic, MqttMessage mqttMessage) { - if (awaitSubTopic == null) { - log.warn("messageArrived on topic: {}", requestTopic); - qoS = mqttMessage.getQos(); - payloadBytes = mqttMessage.getPayload(); - subscribeLatch.countDown(); - } else { - messageArrivedOnAwaitSubTopic(requestTopic, mqttMessage); - } - } - - protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) { - log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); - if (awaitSubTopic.equals(requestTopic)) { - qoS = mqttMessage.getQos(); - payloadBytes = mqttMessage.getPayload(); - subscribeLatch.countDown(); - } + log.warn("messageArrived on topic: {}", requestTopic); + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + subscribeLatch.countDown(); } @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestSubscribeOnTopicCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestSubscribeOnTopicCallback.java new file mode 100644 index 0000000000..0f3cc14629 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestSubscribeOnTopicCallback.java @@ -0,0 +1,45 @@ +/** + * 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. + */ +package org.thingsboard.server.transport.mqtt.mqttv3; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +@Data +@Slf4j +@EqualsAndHashCode(callSuper = true) +public class MqttTestSubscribeOnTopicCallback extends MqttTestCallback { + + protected final String awaitSubTopic; + + public MqttTestSubscribeOnTopicCallback(String awaitSubTopic) { + super(); + this.awaitSubTopic = awaitSubTopic; + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) { + log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); + if (awaitSubTopic.equals(requestTopic)) { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + subscribeLatch.countDown(); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java index 941e97e500..71d8809e38 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java @@ -45,6 +45,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; import java.util.ArrayList; @@ -358,7 +359,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt String update = getWsClient().waitForUpdate(); assertThat(update).as("ws update received").isNotBlank(); - MqttTestCallback callback = new MqttTestCallback(attrSubTopic.replace("+", "1")); + MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(attrSubTopic.replace("+", "1")); client.setCallback(callback); String payloadStr = "{\"clientKeys\":\"" + clientKeysStr + "\", \"sharedKeys\":\"" + sharedKeysStr + "\"}"; client.publishAndWait(attrReqTopicPrefix + "1", payloadStr.getBytes()); @@ -389,7 +390,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt String update = getWsClient().waitForUpdate(); assertThat(update).as("ws update received").isNotBlank(); - MqttTestCallback callback = new MqttTestCallback(attrSubTopic.replace("+", "1")); + MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(attrSubTopic.replace("+", "1")); client.setCallback(callback); TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder(); attributesRequestBuilder.setClientKeys(clientKeysStr); @@ -448,14 +449,14 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt client.subscribeAndWait(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE); //RequestAttributes does not make any subscriptions in device actor - MqttTestCallback clientAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); + MqttTestCallback clientAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(clientAttributesCallback); String csKeysStr = "[\"clientStr\", \"clientBool\", \"clientDbl\", \"clientLong\", \"clientJson\"]"; String csRequestPayloadStr = "{\"id\": 1, \"device\": \"" + deviceName + "\", \"client\": true, \"keys\": " + csKeysStr + "}"; client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, csRequestPayloadStr.getBytes()); validateJsonResponseGateway(clientAttributesCallback, deviceName, CLIENT_ATTRIBUTES_PAYLOAD); - MqttTestCallback sharedAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); + MqttTestCallback sharedAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(sharedAttributesCallback); String shKeysStr = "[\"sharedStr\", \"sharedBool\", \"sharedDbl\", \"sharedLong\", \"sharedJson\"]"; String shRequestPayloadStr = "{\"id\": 1, \"device\": \"" + deviceName + "\", \"client\": false, \"keys\": " + shKeysStr + "}"; @@ -502,13 +503,13 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt client.subscribeAndWait(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE); awaitForDeviceActorToReceiveSubscription(device.getId(), FeatureType.ATTRIBUTES, 1); - MqttTestCallback clientAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); + MqttTestCallback clientAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(clientAttributesCallback); TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(deviceName, clientKeysList, true); client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, gatewayAttributesRequestMsg.toByteArray()); validateProtoClientResponseGateway(clientAttributesCallback, deviceName); - MqttTestCallback sharedAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); + MqttTestCallback sharedAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(sharedAttributesCallback); gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(deviceName, sharedKeysList, false); client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, gatewayAttributesRequestMsg.toByteArray()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/client/AbstractMqttClientConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/client/AbstractMqttClientConnectionTest.java index 116459e5e6..8c567e402c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/client/AbstractMqttClientConnectionTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/client/AbstractMqttClientConnectionTest.java @@ -34,7 +34,7 @@ public abstract class AbstractMqttClientConnectionTest extends AbstractMqttInteg try { client.connectAndWait("wrongAccessToken"); } catch (MqttException e) { - Assert.assertEquals(MqttException.REASON_CODE_FAILED_AUTHENTICATION, e.getReasonCode()); + Assert.assertEquals(MqttException.REASON_CODE_NOT_AUTHORIZED, e.getReasonCode()); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java index 87337545fc..8f0dc24d7c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java @@ -124,7 +124,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { mqttTestClient.connectAndWait(USER_NAME3, "WRONG PASSWORD"); Assert.fail(); // This should not happens, because we have a wrong password } catch (MqttException e) { - Assert.assertEquals(4, e.getReasonCode()); // 4 - Reason code for bad username or password in MQTT v3 + Assert.assertEquals(5, e.getReasonCode()); // 4 - Reason code not authorized in MQTT v3 } Assertions.assertThrows(MqttException.class, () -> { testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java index 9081baa420..de0a3d6f3c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java @@ -35,6 +35,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; import java.util.concurrent.TimeUnit; @@ -270,7 +271,7 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { String provisionRequestMsg = createTestProvisionMessage(deviceCredentials); MqttTestClient client = new MqttTestClient(); client.connectAndWait("provision"); - MqttTestCallback onProvisionCallback = new MqttTestCallback(DEVICE_PROVISION_RESPONSE_TOPIC); + MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC); client.setCallback(onProvisionCallback); client.subscribe(DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE); client.publishAndWait(DEVICE_PROVISION_REQUEST_TOPIC, provisionRequestMsg.getBytes()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java index fcea0f249b..c6d013ba20 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; import java.util.concurrent.TimeUnit; @@ -269,7 +270,7 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { protected byte[] createMqttClientAndPublish(byte[] provisionRequestMsg) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait("provision"); - MqttTestCallback onProvisionCallback = new MqttTestCallback(DEVICE_PROVISION_RESPONSE_TOPIC); + MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC); client.setCallback(onProvisionCallback); client.subscribe(DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE); client.publishAndWait(DEVICE_PROVISION_REQUEST_TOPIC, provisionRequestMsg); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 8537ee9fd8..e1f20a3d95 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; import java.util.ArrayList; @@ -81,7 +82,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM protected void processOneWayRpcTest(String rpcSubTopic) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); - MqttTestCallback callback = new MqttTestCallback(rpcSubTopic.replace("+", "0")); + MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(rpcSubTopic.replace("+", "0")); client.setCallback(callback); subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC); @@ -221,7 +222,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM ); assertNotNull(savedDevice); - MqttTestCallback callback = new MqttTestCallback(GATEWAY_RPC_TOPIC); + MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(GATEWAY_RPC_TOPIC); client.setCallback(callback); subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC); @@ -320,7 +321,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM } } - protected class MqttTestRpcJsonCallback extends MqttTestCallback { + protected class MqttTestRpcJsonCallback extends MqttTestSubscribeOnTopicCallback { private final MqttTestClient client; @@ -330,7 +331,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM } @Override - protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) { + public void messageArrived(String requestTopic, MqttMessage mqttMessage) { log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); if (awaitSubTopic.equals(requestTopic)) { qoS = mqttMessage.getQos(); @@ -349,9 +350,10 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM subscribeLatch.countDown(); } } + } - protected class MqttTestRpcProtoCallback extends MqttTestCallback { + protected class MqttTestRpcProtoCallback extends MqttTestSubscribeOnTopicCallback { private final MqttTestClient client; @@ -361,7 +363,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM } @Override - protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) { + public void messageArrived(String requestTopic, MqttMessage mqttMessage) { log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); if (awaitSubTopic.equals(requestTopic)) { qoS = mqttMessage.getQos(); @@ -380,6 +382,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM subscribeLatch.countDown(); } } + } protected byte[] processProtoMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { @@ -446,7 +449,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM @Override public void messageArrived(String requestTopic, MqttMessage mqttMessage) { - log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); + log.warn("messageArrived on topic: {}", requestTopic); expected.add(new String(mqttMessage.getPayload())); String responseTopic = requestTopic.replace("request", "response"); qoS = mqttMessage.getQos(); diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 953b7094a4..14db2eea7b 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -28,6 +28,12 @@ + + + + + + diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index 9c4c962dc2..36f3dddc3d 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -26,14 +26,19 @@ import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import redis.clients.jedis.JedisPoolConfig; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; @Configuration @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @@ -41,6 +46,9 @@ import java.time.Duration; @Data public abstract class TBRedisCacheConfiguration { + private static final String COMMA = ","; + private static final String COLON = ":"; + @Value("${redis.evictTtlInMs:60000}") private int evictTtlInMs; @@ -126,4 +134,19 @@ public abstract class TBRedisCacheConfiguration { poolConfig.setBlockWhenExhausted(blockWhenExhausted); return poolConfig; } + + protected List getNodes(String nodes) { + List result; + if (StringUtils.isBlank(nodes)) { + result = Collections.emptyList(); + } else { + result = new ArrayList<>(); + for (String hostPort : nodes.split(COMMA)) { + String host = hostPort.split(COLON)[0]; + int port = Integer.parseInt(hostPort.split(COLON)[1]); + result.add(new RedisNode(host, port)); + } + } + return result; + } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java index dfc6ebd225..0a378103b0 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java @@ -20,22 +20,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; -import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.thingsboard.server.common.data.StringUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; @Configuration @ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class) @ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster") public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { - private static final String COMMA = ","; - private static final String COLON = ":"; - @Value("${redis.cluster.nodes:}") private String clusterNodes; @@ -59,19 +50,4 @@ public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { return new JedisConnectionFactory(clusterConfiguration, buildPoolConfig()); } } - - private List getNodes(String nodes) { - List result; - if (StringUtils.isBlank(nodes)) { - result = Collections.emptyList(); - } else { - result = new ArrayList<>(); - for (String hostPort : nodes.split(COMMA)) { - String host = hostPort.split(COLON)[0]; - Integer port = Integer.valueOf(hostPort.split(COLON)[1]); - result.add(new RedisNode(host, port)); - } - } - return result; - } -} \ No newline at end of file +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java new file mode 100644 index 0000000000..78cb445d82 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java @@ -0,0 +1,62 @@ +/** + * 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. + */ +package org.thingsboard.server.cache; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + +@Configuration +@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class) +@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "sentinel") +public class TBRedisSentinelConfiguration extends TBRedisCacheConfiguration { + + @Value("${redis.sentinel.master:}") + private String master; + + @Value("${redis.sentinel.sentinels:}") + private String sentinels; + + @Value("${redis.sentinel.password:}") + private String sentinelPassword; + + @Value("${redis.sentinel.useDefaultPoolConfig:true}") + private boolean useDefaultPoolConfig; + + @Value("${redis.db:}") + private Integer database; + + @Value("${redis.password:}") + private String password; + + public JedisConnectionFactory loadFactory() { + RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); + redisSentinelConfiguration.setMaster(master); + redisSentinelConfiguration.setSentinels(getNodes(sentinels)); + redisSentinelConfiguration.setSentinelPassword(sentinelPassword); + redisSentinelConfiguration.setPassword(password); + redisSentinelConfiguration.setDatabase(database); + if (useDefaultPoolConfig) { + return new JedisConnectionFactory(redisSentinelConfiguration); + } else { + return new JedisConnectionFactory(redisSentinelConfiguration, buildPoolConfig()); + } + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index 5e05cdddc6..6f0e362209 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -39,9 +40,9 @@ public interface ResourceService extends EntityDaoService { ListenableFuture findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId); - PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); - PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); List findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType lwm2mModel, String[] objectIds); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java index 5e13e19b04..fd42df8e4a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.databind.ObjectMapper; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.UUIDBased; @@ -23,6 +24,7 @@ import java.io.Serializable; public abstract class BaseData extends IdBased implements Serializable { private static final long serialVersionUID = 5422817607129962637L; + public static final ObjectMapper mapper = new ObjectMapper(); protected long createdTime; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java index b287956e4a..7e3f47aed4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Objects; import java.util.function.Consumer; @@ -36,7 +37,6 @@ import java.util.function.Supplier; @Slf4j public abstract class BaseDataWithAdditionalInfo extends BaseData implements HasAdditionalInfo { - public static final ObjectMapper mapper = new ObjectMapper(); @NoXss private transient JsonNode additionalInfo; @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java index d8904f03e6..bbc54eb4a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) -public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasEmail { +public abstract class ContactBased extends BaseDataWithAdditionalInfo implements HasEmail { private static final long serialVersionUID = 5047448057830660988L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index d194fb7755..636ab0de38 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -164,11 +164,6 @@ public class Customer extends ContactBased implements HasTenantId, E return title; } - @Override - public String getSearchText() { - return getTitle(); - } - @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index 3c791171d5..a32c16d88a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -30,7 +30,7 @@ import java.util.Objects; import java.util.Set; @ApiModel -public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId, HasTitle { +public class DashboardInfo extends BaseData implements HasName, HasTenantId, HasTitle { private TenantId tenantId; @NoXss @@ -186,11 +186,6 @@ public class DashboardInfo extends SearchTextBased implements HasNa return title; } - @Override - public String getSearchText() { - return getTitle(); - } - @Override public int hashCode() { final int prime = 31; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 7ff7d72023..196edadf9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -36,14 +36,12 @@ import javax.validation.Valid; import java.io.ByteArrayInputStream; import java.io.IOException; -import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; - @ApiModel @Data @ToString(exclude = {"image", "profileDataBytes"}) @EqualsAndHashCode(callSuper = true) @Slf4j -public class DeviceProfile extends SearchTextBased implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity { +public class DeviceProfile extends BaseData implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity { private static final long serialVersionUID = 6998485460273302018L; @@ -139,11 +137,6 @@ public class DeviceProfile extends SearchTextBased implements H return super.getCreatedTime(); } - @Override - public String getSearchText() { - return getName(); - } - @ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the device profile is not specified during device creation.") public boolean isDefault() { return isDefault; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index 90d05c8f29..c0e44baeb3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public class EntityView extends SearchTextBasedWithAdditionalInfo +public class EntityView extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, ExportableEntity { private static final long serialVersionUID = 5582010124562018986L; @@ -82,11 +82,6 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo this.externalId = entityView.getExternalId(); } - @Override - public String getSearchText() { - return getName() /*What the ...*/; - } - @ApiModelProperty(position = 4, value = "JSON object with Customer Id. Use 'assignEntityViewToCustomer' to change the Customer Id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @Override public CustomerId getCustomerId() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index c6033c5632..ab84724117 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasTitle { +public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle { private static final long serialVersionUID = 3168391583570815419L; @@ -118,11 +118,6 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo extends BaseData { - - private static final long serialVersionUID = -539812997348227609L; - - public SearchTextBased() { - super(); - } - - public SearchTextBased(I id) { - super(id); - } - - public SearchTextBased(SearchTextBased searchTextBased) { - super(searchTextBased); - } - - @JsonIgnore - public abstract String getSearchText(); - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java deleted file mode 100644 index 9c89d3c9fe..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java +++ /dev/null @@ -1,108 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.common.data; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.id.UUIDBased; -import org.thingsboard.server.common.data.validation.NoXss; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * Created by ashvayka on 19.02.18. - */ -@Slf4j -public abstract class SearchTextBasedWithAdditionalInfo extends SearchTextBased implements HasAdditionalInfo { - - public static final ObjectMapper mapper = new ObjectMapper(); - @NoXss - private transient JsonNode additionalInfo; - @JsonIgnore - private byte[] additionalInfoBytes; - - public SearchTextBasedWithAdditionalInfo() { - super(); - } - - public SearchTextBasedWithAdditionalInfo(I id) { - super(id); - } - - public SearchTextBasedWithAdditionalInfo(SearchTextBasedWithAdditionalInfo searchTextBased) { - super(searchTextBased); - setAdditionalInfo(searchTextBased.getAdditionalInfo()); - } - - @Override - public JsonNode getAdditionalInfo() { - return getJson(() -> additionalInfo, () -> additionalInfoBytes); - } - - public void setAdditionalInfo(JsonNode addInfo) { - setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - SearchTextBasedWithAdditionalInfo that = (SearchTextBasedWithAdditionalInfo) o; - return Arrays.equals(additionalInfoBytes, that.additionalInfoBytes); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), additionalInfoBytes); - } - - public static JsonNode getJson(Supplier jsonData, Supplier binaryData) { - JsonNode json = jsonData.get(); - if (json != null) { - return json; - } else { - byte[] data = binaryData.get(); - if (data != null) { - try { - return mapper.readTree(new ByteArrayInputStream(data)); - } catch (IOException e) { - log.warn("Can't deserialize json data: ", e); - return null; - } - } else { - return null; - } - } - } - - public static void setJson(JsonNode json, Consumer jsonConsumer, Consumer bytesConsumer) { - jsonConsumer.accept(json); - try { - bytesConsumer.accept(mapper.writeValueAsBytes(json)); - } catch (JsonProcessingException e) { - log.warn("Can't serialize json data: ", e); - } - } -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 3f4aa11c1f..a7671f4327 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -24,6 +24,9 @@ import java.util.Base64; import static org.apache.commons.lang3.StringUtils.repeat; public class StringUtils { + + private static final int DEFAULT_TOKEN_LENGTH = 8; + public static final SecureRandom RANDOM = new SecureRandom(); public static final String EMPTY = ""; @@ -205,4 +208,8 @@ public class StringUtils { return encoder.encodeToString(bytes); } + public static String generateSafeToken() { + return generateSafeToken(DEFAULT_TOKEN_LENGTH); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 91bb7ba383..8b82db196a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -74,6 +74,8 @@ public class TbResource extends TbResourceInfo { builder.append(fileName); builder.append(", data="); builder.append(data); + builder.append(", hashCode="); + builder.append(getEtag()); builder.append("]"); return builder.toString(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index 1f634bcb0e..af3c71f226 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class TbResourceInfo extends SearchTextBased implements HasName, HasTenantId { +public class TbResourceInfo extends BaseData implements HasName, HasTenantId { private static final long serialVersionUID = 7282664529021651736L; @@ -48,6 +48,8 @@ public class TbResourceInfo extends SearchTextBased implements Has private String resourceKey; @ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String searchText; + @ApiModelProperty(position = 8, value = "Resource etag.", example = "33a64df551425fcc55e4d42a148795d9f25f89d4", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private String etag; public TbResourceInfo() { super(); @@ -64,6 +66,7 @@ public class TbResourceInfo extends SearchTextBased implements Has this.resourceType = resourceInfo.getResourceType(); this.resourceKey = resourceInfo.getResourceKey(); this.searchText = resourceInfo.getSearchText(); + this.etag = resourceInfo.getEtag(); } @ApiModelProperty(position = 1, value = "JSON object with the Resource Id. " + @@ -87,7 +90,7 @@ public class TbResourceInfo extends SearchTextBased implements Has return title; } - @Override + @JsonIgnore public String getSearchText() { return searchText != null ? searchText : title; } @@ -107,6 +110,8 @@ public class TbResourceInfo extends SearchTextBased implements Has builder.append(resourceType); builder.append(", resourceKey="); builder.append(resourceKey); + builder.append(", hashCode="); + builder.append(etag); builder.append("]"); return builder.toString(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java new file mode 100644 index 0000000000..9057fc144b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java @@ -0,0 +1,29 @@ +/** + * 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. + */ +package org.thingsboard.server.common.data; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@Builder +public class TbResourceInfoFilter { + + private TenantId tenantId; + private ResourceType resourceType; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index 7e5210e344..89cd9ec467 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -96,11 +96,6 @@ public class Tenant extends ContactBased implements HasTenantId, HasTi this.tenantProfileId = tenantProfileId; } - @Override - public String getSearchText() { - return getTitle(); - } - @ApiModelProperty(position = 1, value = "JSON object with the tenant Id. " + "Specify this field to update the tenant. " + "Referencing non-existing tenant Id will cause error. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index 91502692a6..5ce6c00fb8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -33,17 +34,17 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Optional; -import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; - @ApiModel @Data @ToString(exclude = {"profileDataBytes"}) @EqualsAndHashCode(callSuper = true) @Slf4j -public class TenantProfile extends SearchTextBased implements HasName { +public class TenantProfile extends BaseData implements HasName { private static final long serialVersionUID = 3021989561267192281L; + public static final ObjectMapper mapper = new ObjectMapper(); + @NoXss @Length(fieldName = "name") @ApiModelProperty(position = 3, value = "Name of the tenant profile", example = "High Priority Tenants") @@ -93,11 +94,6 @@ public class TenantProfile extends SearchTextBased implements H return super.getCreatedTime(); } - @Override - public String getSearchText() { - return getName(); - } - @Override public String getName() { return name; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index f629151d18..b3d9268c6b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -34,7 +34,7 @@ import static org.apache.commons.lang3.StringUtils.isNotEmpty; @ApiModel @EqualsAndHashCode(callSuper = true) -public class User extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, NotificationRecipient { +public class User extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, NotificationRecipient { private static final long serialVersionUID = 8250339805336035966L; @@ -162,11 +162,6 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H return super.getAdditionalInfo(); } - @Override - public String getSearchText() { - return getEmail(); - } - @JsonIgnore public String getTitle() { return getTitle(email, firstName, lastName); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 438abb0db2..bbb56305db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -21,11 +21,11 @@ import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; @@ -37,7 +37,7 @@ import java.util.Optional; @ApiModel @EqualsAndHashCode(callSuper = true) -public class Asset extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, ExportableEntity { +public class Asset extends BaseDataWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, ExportableEntity { private static final long serialVersionUID = 2807343040519543363L; @@ -158,12 +158,6 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.assetProfileId = assetProfileId; } - - @Override - public String getSearchText() { - return getName(); - } - @ApiModelProperty(position = 9, value = "Additional parameters of the asset", dataType = "com.fasterxml.jackson.databind.JsonNode") @Override public JsonNode getAdditionalInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java index 310b665ed0..5456ba36ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java @@ -21,11 +21,12 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -38,7 +39,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ToString(exclude = {"image"}) @EqualsAndHashCode(callSuper = true) @Slf4j -public class AssetProfile extends SearchTextBased implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity { +public class AssetProfile extends BaseData implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity { private static final long serialVersionUID = 6998485460273302018L; @@ -112,11 +113,6 @@ public class AssetProfile extends SearchTextBased implements Has return super.getCreatedTime(); } - @Override - public String getSearchText() { - return getName(); - } - @ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the asset profile is not specified during asset creation.") public boolean isDefault(){ return isDefault; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index fe9617c6c2..6d1d9390ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -20,10 +20,10 @@ import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import lombok.Setter; import lombok.ToString; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) @ToString @Setter -public class Edge extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId { +public class Edge extends BaseDataWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId { private static final long serialVersionUID = 4934987555236873728L; @@ -137,11 +137,6 @@ public class Edge extends SearchTextBasedWithAdditionalInfo implements H return this.label; } - @Override - public String getSearchText() { - return getName(); - } - @ApiModelProperty(position = 9, required = true, value = "Edge routing key ('username') to authorize on cloud") public String getRoutingKey() { return this.routingKey; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java index 7b11e86c9f..37934e1f50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java index 3696aa0582..c7a65f49d2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java index d9294b7fba..988b92600a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java index 85d26ffde0..435da5e3c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java index 6570d76c57..7e0119c7f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java index 4ebc531c4b..866f50f5d9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -34,6 +35,7 @@ public class QueueId extends UUIDBased implements EntityId { return new QueueId(UUID.fromString(queueId)); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "QUEUE", allowableValues = "QUEUE") @Override public EntityType getEntityType() { return EntityType.QUEUE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mail/MailOauth2Provider.java b/common/data/src/main/java/org/thingsboard/server/common/data/mail/MailOauth2Provider.java new file mode 100644 index 0000000000..c0cdb92500 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mail/MailOauth2Provider.java @@ -0,0 +1,31 @@ +/** + * 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. + */ +package org.thingsboard.server.common.data.mail; + +public enum MailOauth2Provider { + GOOGLE("Google"), OFFICE_365("Office 365"), SENDGRID("SendGrid"), CUSTOM("Custom"); + + public final String label; + + MailOauth2Provider(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java index 31e05a2cb3..619e1ad38f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java @@ -62,6 +62,9 @@ public class NotificationRequestStats { return; } String errorMessage = error.getMessage(); + if (errorMessage == null) { + errorMessage = error.getClass().getSimpleName(); + } errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java index 5ca8dff863..c88ade5bb1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java @@ -36,6 +36,8 @@ import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; @Data @NoArgsConstructor @@ -84,4 +86,12 @@ public class NotificationRule extends BaseData implements Ha triggerType == recipientsConfig.getTriggerType(); } + @JsonIgnore + public String getDeduplicationKey() { + String targets = recipientsConfig.getTargetsTable().values().stream() + .flatMap(List::stream).sorted().map(Object::toString) + .collect(Collectors.joining(",")); + return String.join(":", targets, triggerConfig.getDeduplicationKey()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java index 3406a802c7..b0eec28858 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; @@ -39,4 +40,9 @@ public interface NotificationRuleTriggerConfig extends Serializable { NotificationRuleTriggerType getTriggerType(); + @JsonIgnore + default String getDeduplicationKey() { + return "#"; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java index e79e7f1195..dff86f4ba1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java @@ -16,10 +16,8 @@ package org.thingsboard.server.common.data.notification.rule.trigger; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor public enum NotificationRuleTriggerType { ENTITY_ACTION, @@ -28,15 +26,18 @@ public enum NotificationRuleTriggerType { ALARM_ASSIGNMENT, DEVICE_ACTIVITY, RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, - NEW_PLATFORM_VERSION(false, true), - ENTITIES_LIMIT(false, false), - API_USAGE_LIMIT(false, false); + NEW_PLATFORM_VERSION(false), + ENTITIES_LIMIT(false), + API_USAGE_LIMIT(false); private final boolean tenantLevel; - private final boolean deduplicate; NotificationRuleTriggerType() { - this(true, false); + this(true); + } + + NotificationRuleTriggerType(boolean tenantLevel) { + this.tenantLevel = tenantLevel; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/SearchTextEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/TriggerTypeConfig.java similarity index 76% rename from dao/src/main/java/org/thingsboard/server/dao/model/SearchTextEntity.java rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/TriggerTypeConfig.java index e14fe25504..6991bb1277 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/SearchTextEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/TriggerTypeConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model; +package org.thingsboard.server.common.data.notification.settings; -public interface SearchTextEntity extends BaseEntity { +import lombok.Data; - String getSearchTextSource(); - - void setSearchText(String searchText); - +@Data +public class TriggerTypeConfig { + private long deduplicationDuration; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java index 988af25d92..49cebc83e0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -21,8 +21,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.validation.Length; @@ -34,7 +34,7 @@ import java.util.List; @ToString @NoArgsConstructor @ApiModel -public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo implements HasName { +public class OAuth2ClientRegistrationTemplate extends BaseDataWithAdditionalInfo implements HasName { @Length(fieldName = "providerId") @ApiModelProperty(value = "OAuth2 provider identifier (e.g. its name)", required = true) @@ -95,9 +95,4 @@ public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditio public String getName() { return providerId; } - - @Override - public String getSearchText() { - return getName(); - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java index c1e92e8009..8d4e34f3ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java @@ -20,8 +20,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.OAuth2ParamsId; import org.thingsboard.server.common.data.id.OAuth2RegistrationId; @@ -31,7 +31,7 @@ import java.util.List; @Data @ToString(exclude = {"clientSecret"}) @NoArgsConstructor -public class OAuth2Registration extends SearchTextBasedWithAdditionalInfo implements HasName { +public class OAuth2Registration extends BaseDataWithAdditionalInfo implements HasName { private OAuth2ParamsId oauth2ParamsId; private OAuth2MapperConfig mapperConfig; @@ -71,9 +71,4 @@ public class OAuth2Registration extends SearchTextBasedWithAdditionalInfo { +public class ComponentDescriptor extends BaseData { private static final long serialVersionUID = 1L; @@ -90,11 +91,6 @@ public class ComponentDescriptor extends SearchTextBased return super.getCreatedTime(); } - @Override - public String getSearchText() { - return name; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index f757998d04..6839c60bcd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -16,9 +16,9 @@ package org.thingsboard.server.common.data.queue; import lombok.Data; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @Data -public class Queue extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId { +public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId { private TenantId tenantId; @NoXss @Length(fieldName = "name") @@ -60,9 +60,4 @@ public class Queue extends SearchTextBasedWithAdditionalInfo implements this.processingStrategy = queueConfiguration.getProcessingStrategy(); setAdditionalInfo(queueConfiguration.getAdditionalInfo()); } - - @Override - public String getSearchText() { - return getName(); - } } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index f66a9338c8..fa2d52f3f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.validation.Length; @@ -111,11 +111,11 @@ public class EntityRelation implements Serializable { @ApiModelProperty(position = 5, value = "Additional parameters of the relation", dataType = "com.fasterxml.jackson.databind.JsonNode") public JsonNode getAdditionalInfo() { - return SearchTextBasedWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); + return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); } public void setAdditionalInfo(JsonNode addInfo) { - SearchTextBasedWithAdditionalInfo.setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); + BaseDataWithAdditionalInfo.setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 4898a95a1b..edb70cefda 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -22,10 +22,10 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data @EqualsAndHashCode(callSuper = true) @Slf4j -public class RuleChain extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, ExportableEntity { +public class RuleChain extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, ExportableEntity { private static final long serialVersionUID = -5656679015121935465L; @@ -81,11 +81,6 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im this.setExternalId(ruleChain.getExternalId()); } - @Override - public String getSearchText() { - return getName(); - } - @Override public String getName() { return name; @@ -107,7 +102,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im } public JsonNode getConfiguration() { - return SearchTextBasedWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes); + return BaseDataWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes); } public void setConfiguration(JsonNode data) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java index f1b55b0e7a..3fc1ceb628 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java @@ -22,8 +22,8 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.validation.Length; @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data @EqualsAndHashCode(callSuper = true) @Slf4j -public class RuleNode extends SearchTextBasedWithAdditionalInfo implements HasName { +public class RuleNode extends BaseDataWithAdditionalInfo implements HasName { private static final long serialVersionUID = -5656679015121235465L; @@ -78,18 +78,13 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo impl this.externalId = ruleNode.getExternalId(); } - @Override - public String getSearchText() { - return getName(); - } - @Override public String getName() { return name; } public JsonNode getConfiguration() { - return SearchTextBasedWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes); + return BaseDataWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes); } public void setConfiguration(JsonNode data) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java index 9a4282aeb7..29a64bed9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java @@ -23,8 +23,8 @@ import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.validation.NoXss; -import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.getJson; -import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.setJson; +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson; @EqualsAndHashCode(callSuper = true) public class UserCredentials extends BaseData { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java index 3d345b5a1a..cf48d9c215 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java @@ -26,8 +26,8 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; -import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.getJson; -import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.setJson; +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson; @ApiModel @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java index d8d17613de..a0736ca50e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -51,20 +50,19 @@ public class CollectionsUtil { } @SuppressWarnings("unchecked") - public static Map mapOf(Object... kvs) { - Map map = new HashMap<>(); + public static Map mapOf(T... kvs) { + if (kvs.length % 2 != 0) { + throw new IllegalArgumentException("Invalid number of parameters"); + } + Map map = new HashMap<>(); for (int i = 0; i < kvs.length; i += 2) { - K key = (K) kvs[i]; - V value = (V) kvs[i + 1]; + T key = kvs[i]; + T value = kvs[i + 1]; map.put(key, value); } return map; } - public static Map unmodifiableMapOf(Object... kvs) { - return Collections.unmodifiableMap(mapOf(kvs)); - } - public static boolean emptyOrContains(Collection collection, V element) { return isEmpty(collection) || collection.contains(element); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index 0c0f98c9d3..0ceef991ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -21,11 +21,12 @@ import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTitle; -import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.validation.Length; @@ -33,7 +34,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @EqualsAndHashCode(callSuper = true) -public class WidgetsBundle extends SearchTextBased implements HasName, HasTenantId, ExportableEntity, HasTitle { +public class WidgetsBundle extends BaseData implements HasName, HasTenantId, ExportableEntity, HasTitle { private static final long serialVersionUID = -7627368878362410489L; @@ -106,11 +107,6 @@ public class WidgetsBundle extends SearchTextBased implements H return super.getCreatedTime(); } - @Override - public String getSearchText() { - return getTitle(); - } - @ApiModelProperty(position = 3, value = "Same as title of the Widget Bundle. Read-only field. Update the 'title' to change the 'name' of the Widget Bundle.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java index 9010fc0b54..1b1cbcdf54 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java @@ -64,7 +64,10 @@ public final class TbMsgProcessingCtx implements Serializable { } public TbMsgProcessingStackItem pop() { - return !stack.isEmpty() ? stack.removeLast() : null; + if (stack == null || stack.isEmpty()) { + return null; + } + return stack.removeLast(); } public static TbMsgProcessingCtx fromProto(MsgProtos.TbMsgProcessingCtxProto ctx) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java similarity index 93% rename from common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java rename to common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java index 1fa9d82863..380773c1b0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.queue.notification; +package org.thingsboard.server.common.msg.notification; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineMsgTrigger.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmAssignmentTrigger.java similarity index 71% rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineMsgTrigger.java rename to common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmAssignmentTrigger.java index 361264a0c2..98ed43f886 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineMsgTrigger.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmAssignmentTrigger.java @@ -17,30 +17,30 @@ package org.thingsboard.server.common.msg.notification.trigger; import lombok.Builder; import lombok.Data; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; -import org.thingsboard.server.common.msg.TbMsg; - -import java.util.Map; @Data @Builder -public class RuleEngineMsgTrigger implements NotificationRuleTrigger { +public class AlarmAssignmentTrigger implements NotificationRuleTrigger { private final TenantId tenantId; - private final TbMsg msg; - - public static Map msgTypeToTriggerType; // set on init by DefaultNotificationRuleProcessor + private final AlarmInfo alarmInfo; + private final ActionType actionType; + private final User user; @Override - public NotificationRuleTriggerType getType() { - return msgTypeToTriggerType != null ? msgTypeToTriggerType.get(msg.getType()) : null; + public EntityId getOriginatorEntityId() { + return alarmInfo.getOriginator(); } @Override - public EntityId getOriginatorEntityId() { - return msg.getOriginator(); + public NotificationRuleTriggerType getType() { + return NotificationRuleTriggerType.ALARM_ASSIGNMENT; } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmCommentTrigger.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmCommentTrigger.java new file mode 100644 index 0000000000..d0b3bdd5de --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmCommentTrigger.java @@ -0,0 +1,48 @@ +/** + * 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. + */ +package org.thingsboard.server.common.msg.notification.trigger; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; + +@Data +@Builder +public class AlarmCommentTrigger implements NotificationRuleTrigger { + + private final TenantId tenantId; + private final AlarmComment comment; + private final Alarm alarm; + private final ActionType actionType; + private final User user; + + @Override + public NotificationRuleTriggerType getType() { + return NotificationRuleTriggerType.ALARM_COMMENT; + } + + @Override + public EntityId getOriginatorEntityId() { + return alarm.getId(); + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/ApiUsageLimitTrigger.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/ApiUsageLimitTrigger.java index 368a16712c..f21d3077ca 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/ApiUsageLimitTrigger.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/ApiUsageLimitTrigger.java @@ -36,11 +36,6 @@ public class ApiUsageLimitTrigger implements NotificationRuleTrigger { return NotificationRuleTriggerType.API_USAGE_LIMIT; } - @Override - public TenantId getTenantId() { - return tenantId; - } - @Override public EntityId getOriginatorEntityId() { return tenantId; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/DeviceActivityTrigger.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/DeviceActivityTrigger.java new file mode 100644 index 0000000000..b426b6674b --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/DeviceActivityTrigger.java @@ -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. + */ +package org.thingsboard.server.common.msg.notification.trigger; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; + +@Data +@Builder +public class DeviceActivityTrigger implements NotificationRuleTrigger { + + private final TenantId tenantId; + private final CustomerId customerId; + private final DeviceId deviceId; + private final boolean active; + + private final String deviceName; + private final String deviceType; + private final String deviceLabel; + + @Override + public EntityId getOriginatorEntityId() { + return deviceId; + } + + @Override + public NotificationRuleTriggerType getType() { + return NotificationRuleTriggerType.DEVICE_ACTIVITY; + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NewPlatformVersionTrigger.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NewPlatformVersionTrigger.java index 0da2f0c57b..2bb88a708b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NewPlatformVersionTrigger.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NewPlatformVersionTrigger.java @@ -43,4 +43,21 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger { return TenantId.SYS_TENANT_ID; } + + @Override + public boolean deduplicate() { + return true; + } + + @Override + public String getDeduplicationKey() { + return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), + updateInfo.getCurrentVersion(), updateInfo.getLatestVersion()); + } + + @Override + public long getDefaultDeduplicationDuration() { + return 0; + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NotificationRuleTrigger.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NotificationRuleTrigger.java index b511062549..0cfc87a4df 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NotificationRuleTrigger.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NotificationRuleTrigger.java @@ -29,4 +29,18 @@ public interface NotificationRuleTrigger extends Serializable { EntityId getOriginatorEntityId(); + + default boolean deduplicate() { + return false; + } + + default String getDeduplicationKey() { + EntityId originatorEntityId = getOriginatorEntityId(); + return String.join(":", getType().toString(), originatorEntityId.getEntityType().toString(), originatorEntityId.getId().toString()); + } + + default long getDefaultDeduplicationDuration() { + return 0; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 08f2e849f8..14fb0369ee 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -326,7 +326,7 @@ public class HashPartitionService implements PartitionService { final Map> currentMap = new HashMap<>(); services.forEach(serviceInfo -> { for (String serviceTypeStr : serviceInfo.getServiceTypesList()) { - ServiceType serviceType = ServiceType.valueOf(serviceTypeStr.toUpperCase()); + ServiceType serviceType = ServiceType.of(serviceTypeStr); if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) { partitionTopicsMap.keySet().forEach(queueKey -> currentMap.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(serviceInfo)); @@ -389,7 +389,7 @@ public class HashPartitionService implements PartitionService { private void addNode(Map> queueServiceList, ServiceInfo instance) { for (String serviceTypeStr : instance.getServiceTypesList()) { - ServiceType serviceType = ServiceType.valueOf(serviceTypeStr.toUpperCase()); + ServiceType serviceType = ServiceType.of(serviceTypeStr); if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) { partitionTopicsMap.keySet().forEach(key -> { if (key.getType().equals(ServiceType.TB_RULE_ENGINE)) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java index 3c3988389f..617fdbb50e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java @@ -19,7 +19,12 @@ import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Service; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.settings.TriggerTypeConfig; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -30,10 +35,17 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import java.util.EnumMap; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT; @Service @ConditionalOnMissingBean(value = NotificationRuleProcessor.class, ignored = RemoteNotificationRuleProcessor.class) +@ConfigurationProperties(prefix = "notification-system.rules") @RequiredArgsConstructor @Slf4j public class RemoteNotificationRuleProcessor implements NotificationRuleProcessor { @@ -43,10 +55,16 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso private final PartitionService partitionService; private final DataDecodingEncodingService encodingService; + private Map triggerTypesConfigs; + private final ConcurrentMap submittedTriggers = new ConcurrentReferenceHashMap<>(16, SOFT); + @Override public void process(NotificationRuleTrigger trigger) { + if (trigger.deduplicate() && alreadySubmitted(trigger)) { + return; + } try { - log.trace("Submitting notification rule trigger: {}", trigger); + log.debug("Submitting notification rule trigger: {}", trigger); TransportProtos.NotificationRuleProcessorMsg.Builder msg = TransportProtos.NotificationRuleProcessorMsg.newBuilder() .setTrigger(ByteString.copyFrom(encodingService.encode(trigger))); @@ -57,9 +75,52 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso .setNotificationRuleProcessorMsg(msg) .build()), null); }); - } catch (Exception e) { + } catch (Throwable e) { log.error("Failed to submit notification rule trigger: {}", trigger, e); } } + private boolean alreadySubmitted(NotificationRuleTrigger trigger) { + String deduplicationKey = trigger.getDeduplicationKey(); + + AtomicBoolean alreadySubmitted = new AtomicBoolean(false); + submittedTriggers.compute(deduplicationKey, (key, lastSubmittedTs) -> { + long currentTs = System.currentTimeMillis(); + if (lastSubmittedTs == null) { + return currentTs; + } else { + long deduplicationDuration = getDeduplicationDuration(trigger); + long passed = currentTs - lastSubmittedTs; + if (deduplicationDuration == 0 || passed <= deduplicationDuration) { + log.trace("Notification rule trigger {} was already submitted {} ms ago, deduplication duration is {} ms. Key: '{}'", + trigger.getType(), passed, deduplicationDuration, deduplicationKey); + alreadySubmitted.set(true); + return lastSubmittedTs; + } else { + return currentTs; + } + } + }); + return alreadySubmitted.get(); + } + + private long getDeduplicationDuration(NotificationRuleTrigger trigger) { + if (triggerTypesConfigs == null) { + triggerTypesConfigs = new EnumMap<>(NotificationRuleTriggerType.class); + } + TriggerTypeConfig triggerTypeConfig = triggerTypesConfigs.computeIfAbsent(trigger.getType(), triggerType -> { + TriggerTypeConfig config = new TriggerTypeConfig(); + config.setDeduplicationDuration(trigger.getDefaultDeduplicationDuration()); + return config; + }); + return triggerTypeConfig.getDeduplicationDuration(); + } + + // set from ConfigurationProperties + public void setTriggerTypesConfigs(Map triggerTypesConfigs) { + if (triggerTypesConfigs != null) { + this.triggerTypesConfigs = new EnumMap<>(triggerTypesConfigs); + } + } + } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 3caaba0ba9..f70967b72d 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -561,18 +561,19 @@ public class DefaultCoapClientContext implements CoapClientContext { @Override public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg msg) { - log.trace("[{}] Received RPC command to device", sessionId); + DeviceId deviceId = state.getDeviceId(); + log.trace("[{}][{}] Received RPC command to device: {}", deviceId, sessionId, msg); if (!isDownlinkAllowed(state)) { - log.trace("[{}] ignore downlink request cause client is sleeping.", state.getDeviceId()); + log.trace("[{}][{}] ignore downlink request cause client is sleeping.", deviceId, sessionId); return; } boolean sent = false; String error = null; boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getRpc()); + int requestId = getNextMsgId(); try { Response response = state.getAdaptor().convertToPublish(msg, state.getConfiguration().getRpcRequestDynamicMessageBuilder()); response.setConfirmable(conRequest); - int requestId = getNextMsgId(); response.setMID(requestId); if (conRequest) { PowerMode powerMode = state.getPowerMode(); @@ -591,6 +592,7 @@ public class DefaultCoapClientContext implements CoapClientContext { transportContext.getScheduler().schedule(() -> { TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(requestId); if (rpcRequestMsg != null) { + log.trace("[{}][{}][{}] Going to send to device actor RPC request TIMEOUT status update due to server timeout ...", deviceId, sessionId, requestId); transportService.process(state.getSession(), msg, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); } }, Math.min(getTimeout(state, powerMode, profileSettings), msg.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS); @@ -598,11 +600,13 @@ public class DefaultCoapClientContext implements CoapClientContext { response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> { TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(id); if (rpcRequestMsg != null) { + log.trace("[{}][{}][{}] Going to send to device actor RPC request DELIVERED status update ...", deviceId, sessionId, requestId); transportService.process(state.getSession(), rpcRequestMsg, RpcStatus.DELIVERED, true, TransportServiceCallback.EMPTY); } }, id -> { TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(id); if (rpcRequestMsg != null) { + log.trace("[{}][{}][{}] Going to send to device actor RPC request TIMEOUT status update ...", deviceId, sessionId, requestId); transportService.process(state.getSession(), msg, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); } })); @@ -626,8 +630,10 @@ public class DefaultCoapClientContext implements CoapClientContext { .setRequestId(msg.getRequestId()).setError(error).build(), TransportServiceCallback.EMPTY); } else if (sent) { if (!conRequest) { + log.trace("[{}][{}][{}] Going to send to device actor non-confirmable RPC request DELIVERED status update ...", deviceId, sessionId, requestId); transportService.process(state.getSession(), msg, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); } else if (msg.getPersisted()) { + log.trace("[{}][{}][{}] Going to send to device actor RPC request SENT status update ...", deviceId, sessionId, requestId); transportService.process(state.getSession(), msg, RpcStatus.SENT, TransportServiceCallback.EMPTY); } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 57d77e8166..6267ae9424 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -194,17 +194,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement processMqttMsg(ctx, message); } else { log.error("[{}] Message decoding failed: {}", sessionId, message.decoderResult().cause().getMessage()); - ctx.close(); + closeCtx(ctx); } } else { log.debug("[{}] Received non mqtt message: {}", sessionId, msg.getClass().getSimpleName()); - ctx.close(); + closeCtx(ctx); } } finally { ReferenceCountUtil.safeRelease(msg); } } + private void closeCtx(ChannelHandlerContext ctx) { + if (!rpcAwaitingAck.isEmpty()) { + log.debug("[{}] Cleanup RPC awaiting ack map due to session close!", sessionId); + rpcAwaitingAck.clear(); + } + ctx.close(); + } + InetSocketAddress getAddress(ChannelHandlerContext ctx) { var address = ctx.channel().attr(MqttTransportService.ADDRESS).get(); if (address == null) { @@ -221,7 +229,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { if (msg.fixedHeader() == null) { log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); - ctx.close(); + closeCtx(ctx); return; } deviceSessionCtx.setChannel(ctx); @@ -258,21 +266,21 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } else { log.debug("[{}] Unsupported topic for provisioning requests: {}!", sessionId, topicName); - ctx.close(); + closeCtx(ctx); } } catch (RuntimeException e) { log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - ctx.close(); + closeCtx(ctx); } catch (AdaptorException e) { log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - ctx.close(); + closeCtx(ctx); } break; case PINGREQ: ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); break; case DISCONNECT: - ctx.close(); + closeCtx(ctx); break; } } @@ -282,7 +290,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (queueSize >= context.getMessageQueueSizePerDeviceLimit()) { log.info("Closing current session because msq queue size for device {} exceed limit {} with msgQueueSize counter {} and actual queue size {}", deviceSessionCtx.getDeviceId(), context.getMessageQueueSizePerDeviceLimit(), queueSize, deviceSessionCtx.getMsgQueueSize()); - ctx.close(); + closeCtx(ctx); return; } @@ -316,7 +324,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } break; case DISCONNECT: - ctx.close(); + closeCtx(ctx); break; case PUBACK: int msgId = ((MqttPubAckMessage) msg).variableHeader().messageId(); @@ -381,7 +389,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } catch (RuntimeException e) { log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); - ctx.close(); + closeCtx(ctx); } catch (AdaptorException e) { log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); sendAckOrCloseSession(ctx, topicName, msgId); @@ -421,7 +429,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } catch (RuntimeException e) { log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); - ctx.close(); + closeCtx(ctx); } catch (AdaptorException | ThingsboardException | InvalidProtocolBufferException e) { log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); sendAckOrCloseSession(ctx, topicName, msgId); @@ -523,7 +531,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement ctx.writeAndFlush(createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.PAYLOAD_FORMAT_INVALID)); } else { log.info("[{}] Closing current session due to invalid publish msg [{}][{}]", sessionId, topicName, msgId); - ctx.close(); + closeCtx(ctx); } } @@ -579,7 +587,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); - ctx.close(); + closeCtx(ctx); } }; } @@ -615,7 +623,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onError(Throwable e) { log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); - ctx.close(); + closeCtx(ctx); } } @@ -650,7 +658,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { log.trace("[{}] Failed to get firmware: {}", sessionId, msg, e); - ctx.close(); + closeCtx(ctx); } } @@ -672,7 +680,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement deviceSessionCtx.getChannel().writeAndFlush(deviceSessionCtx .getPayloadAdaptor() .createMqttPublishMsg(deviceSessionCtx, MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC, error.getBytes())); - ctx.close(); + closeCtx(ctx); } private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) { @@ -922,7 +930,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onError(Throwable e) { log.trace("[{}] Failed to process credentials: {}", address, userName, e); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - ctx.close(); + closeCtx(ctx); } }); } @@ -945,14 +953,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onError(Throwable e) { log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - ctx.close(); + closeCtx(ctx); } }); } catch (Exception e) { context.onAuthFailure(address); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.NOT_AUTHORIZED_5, connectMessage)); log.trace("[{}] X509 auth failure: {}", sessionId, address, e); - ctx.close(); + closeCtx(ctx); } } @@ -1000,7 +1008,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement log.error("[{}] Unexpected Exception", sessionId, cause); } - ctx.close(); + closeCtx(ctx); if (cause instanceof OutOfMemoryError) { log.error("Received critical error. Going to shutdown the service."); System.exit(1); @@ -1082,7 +1090,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } catch (Exception e) { log.trace("[{}][{}] Failed to fetch sparkplugDevice connect, sparkplugTopicName", sessionId, deviceSessionCtx.getDeviceInfo().getDeviceName(), e); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - ctx.close(); + closeCtx(ctx); } } @@ -1139,7 +1147,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } ctx.writeAndFlush(createMqttConnAckMsg(returnCode, connectMessage)); - ctx.close(); + closeCtx(ctx); } else { context.onAuthSuccess(address); deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo()); @@ -1168,7 +1176,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement log.warn("[{}] Failed to submit session event", sessionId, e); } ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - ctx.close(); + closeCtx(ctx); } }); } @@ -1216,12 +1224,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); - deviceSessionCtx.getChannel().close(); + closeCtx(deviceSessionCtx.getChannel()); } @Override public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { - log.trace("[{}] Received RPC command to device", sessionId); + log.trace("[{}][{}] Received RPC command to device: {}", deviceSessionCtx.getDeviceId(), sessionId, rpcRequest); try { if (sparkplugSessionHandler != null) { handleToSparkplugDeviceRpcRequest(rpcRequest); @@ -1232,7 +1240,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement .ifPresent(payload -> sendToDeviceRpcRequest(payload, rpcRequest, deviceSessionCtx.getSessionInfo())); } } catch (Exception e) { - log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); + log.trace("[{}][{}] Failed to convert device RPC command to MQTT msg", deviceSessionCtx.getDeviceId(), sessionId, e); this.sendErrorRpcResponse(deviceSessionCtx.getSessionInfo(), rpcRequest.getRequestId(), ThingsboardErrorCode.INVALID_ARGUMENTS, "Failed to convert device RPC command to MQTT msg: " + rpcRequest.getMethodName() + rpcRequest.getParams()); @@ -1265,30 +1273,35 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void sendToDeviceRpcRequest(MqttMessage payload, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); + int requestId = rpcRequest.getRequestId(); if (isAckExpected(payload)) { rpcAwaitingAck.put(msgId, rpcRequest); context.getScheduler().schedule(() -> { TransportProtos.ToDeviceRpcRequestMsg msg = rpcAwaitingAck.remove(msgId); if (msg != null) { + log.trace("[{}][{}][{}] Going to send to device actor RPC request TIMEOUT status update ...", deviceSessionCtx.getDeviceId(), sessionId, requestId); transportService.process(sessionInfo, rpcRequest, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); } }, Math.max(0, Math.min(deviceSessionCtx.getContext().getTimeout(), rpcRequest.getExpirationTime() - System.currentTimeMillis())), TimeUnit.MILLISECONDS); } var cf = publish(payload, deviceSessionCtx); cf.addListener(result -> { - if (result.cause() == null) { - if (!isAckExpected(payload)) { - transportService.process(sessionInfo, rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); - } else if (rpcRequest.getPersisted()) { - transportService.process(sessionInfo, rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY); - } - if (sparkplugSessionHandler != null) { - this.sendSuccessRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.CONTENT, "Success: " + rpcRequest.getMethodName()); - } - } else { - log.trace("[{}] Failed send To Device Rpc Request [{}]", sessionId, rpcRequest.getMethodName()); - this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + Throwable throwable = result.cause(); + if (throwable != null) { + log.trace("[{}][{}][{}] Failed send RPC request to device due to: ", deviceSessionCtx.getDeviceId(), sessionId, requestId, throwable); + this.sendErrorRpcResponse(sessionInfo, requestId, ThingsboardErrorCode.INVALID_ARGUMENTS, " Failed send To Device Rpc Request: " + rpcRequest.getMethodName()); + return; + } + if (!isAckExpected(payload)) { + log.trace("[{}][{}][{}] Going to send to device actor RPC request DELIVERED status update ...", deviceSessionCtx.getDeviceId(), sessionId, requestId); + transportService.process(sessionInfo, rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); + } else if (rpcRequest.getPersisted()) { + log.trace("[{}][{}][{}] Going to send to device actor RPC request SENT status update ...", deviceSessionCtx.getDeviceId(), sessionId, requestId); + transportService.process(sessionInfo, rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY); + } + if (sparkplugSessionHandler != null) { + this.sendSuccessRpcResponse(sessionInfo, requestId, ResponseCode.CONTENT, "Success: " + rpcRequest.getMethodName()); } }); } @@ -1327,7 +1340,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onDeviceDeleted(DeviceId deviceId) { context.onAuthFailure(address); ChannelHandlerContext ctx = deviceSessionCtx.getChannel(); - ctx.close(); + closeCtx(ctx); } public void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ThingsboardErrorCode result, String errorMsg) { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/ReturnCodeResolver.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/ReturnCodeResolver.java index 5c97286a2d..e4b8e8fc2f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/ReturnCodeResolver.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/ReturnCodeResolver.java @@ -27,7 +27,6 @@ public class ReturnCodeResolver { if (!MqttVersion.MQTT_5.equals(mqttVersion) && !ReturnCode.SUCCESS.equals(returnCode)) { switch (returnCode) { case BAD_USERNAME_OR_PASSWORD: - return MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD; case NOT_AUTHORIZED_5: return MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED; case SERVER_UNAVAILABLE_5: diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 00dcbb2545..102d5f181e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -482,6 +482,7 @@ public class ModelConstants { public static final String RESOURCE_TITLE_COLUMN = TITLE_PROPERTY; public static final String RESOURCE_FILE_NAME_COLUMN = "file_name"; public static final String RESOURCE_DATA_COLUMN = "data"; + public static final String RESOURCE_ETAG_COLUMN = "etag"; /** * Ota Package constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java index 3b7abed70d..bff68a0149 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -40,13 +39,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERT import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; @Data @EqualsAndHashCode(callSuper = true) @TypeDef(name = "json", typeClass = JsonStringType.class) @MappedSuperclass -public abstract class AbstractAssetEntity extends BaseSqlEntity implements SearchTextEntity { +public abstract class AbstractAssetEntity extends BaseSqlEntity { @Column(name = ASSET_TENANT_ID_PROPERTY) private UUID tenantId; @@ -63,9 +61,6 @@ public abstract class AbstractAssetEntity extends BaseSqlEntity @Column(name = ASSET_LABEL_PROPERTY) private String label; - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - @Type(type = "json") @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; @@ -112,25 +107,10 @@ public abstract class AbstractAssetEntity extends BaseSqlEntity this.type = assetEntity.getType(); this.name = assetEntity.getName(); this.label = assetEntity.getLabel(); - this.searchText = assetEntity.getSearchText(); this.additionalInfo = assetEntity.getAdditionalInfo(); this.externalId = assetEntity.getExternalId(); } - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - protected Asset toAsset() { Asset asset = new Asset(new AssetId(id)); asset.setCreatedTime(createdTime); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java index f5a7423df9..3a274d31ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -42,13 +41,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROUTING_KEY_P import static org.thingsboard.server.dao.model.ModelConstants.EDGE_SECRET_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TYPE_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; @Data @EqualsAndHashCode(callSuper = true) @TypeDef(name = "json", typeClass = JsonStringType.class) @MappedSuperclass -public abstract class AbstractEdgeEntity extends BaseSqlEntity implements SearchTextEntity { +public abstract class AbstractEdgeEntity extends BaseSqlEntity { @Column(name = EDGE_TENANT_ID_PROPERTY, columnDefinition = "uuid") private UUID tenantId; @@ -68,9 +66,6 @@ public abstract class AbstractEdgeEntity extends BaseSqlEntity extends BaseSqlEntity extends BaseSqlEntity implements SearchTextEntity { +public abstract class AbstractEntityViewEntity extends BaseSqlEntity { @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY) private UUID entityId; @@ -82,9 +80,6 @@ public abstract class AbstractEntityViewEntity extends Bas @Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY) private long endTs; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Type(type = "json") @Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; @@ -120,7 +115,6 @@ public abstract class AbstractEntityViewEntity extends Bas } this.startTs = entityView.getStartTimeMs(); this.endTs = entityView.getEndTimeMs(); - this.searchText = entityView.getSearchText(); this.additionalInfo = entityView.getAdditionalInfo(); if (entityView.getExternalId() != null) { this.externalId = entityView.getExternalId().getId(); @@ -139,21 +133,10 @@ public abstract class AbstractEntityViewEntity extends Bas this.keys = entityViewEntity.getKeys(); this.startTs = entityViewEntity.getStartTs(); this.endTs = entityViewEntity.getEndTs(); - this.searchText = entityViewEntity.getSearchText(); this.additionalInfo = entityViewEntity.getAdditionalInfo(); this.externalId = entityViewEntity.getExternalId(); } - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - protected EntityView toEntityView() { EntityView entityView = new EntityView(new EntityViewId(getUuid())); entityView.setCreatedTime(createdTime); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java index 39dca43d29..0a8012a55c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -36,14 +35,11 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) @TypeDef(name = "json", typeClass = JsonStringType.class) @MappedSuperclass -public abstract class AbstractTenantEntity extends BaseSqlEntity implements SearchTextEntity { +public abstract class AbstractTenantEntity extends BaseSqlEntity { @Column(name = ModelConstants.TENANT_TITLE_PROPERTY) private String title; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.TENANT_REGION_PROPERTY) private String region; @@ -120,20 +116,6 @@ public abstract class AbstractTenantEntity extends BaseSqlEnti this.tenantProfileId = tenantEntity.getTenantProfileId(); } - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - protected Tenant toTenant() { Tenant tenant = new Tenant(TenantId.fromUUID(this.getUuid())); tenant.setCreatedTime(createdTime); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java index 1deb1e5c7b..1647a7fe37 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; @@ -35,7 +34,7 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.ASSET_PROFILE_TABLE_NAME) -public final class AssetProfileEntity extends BaseSqlEntity implements SearchTextEntity { +public final class AssetProfileEntity extends BaseSqlEntity { @Column(name = ModelConstants.ASSET_PROFILE_TENANT_ID_PROPERTY) private UUID tenantId; @@ -49,9 +48,6 @@ public final class AssetProfileEntity extends BaseSqlEntity implem @Column(name = ModelConstants.ASSET_PROFILE_DESCRIPTION_PROPERTY) private String description; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.ASSET_PROFILE_IS_DEFAULT_PROPERTY) private boolean isDefault; @@ -101,20 +97,6 @@ public final class AssetProfileEntity extends BaseSqlEntity implem } } - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - @Override public AssetProfile toData() { AssetProfile assetProfile = new AssetProfile(new AssetProfileId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java index 968a49507e..a476d69c97 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -41,7 +40,7 @@ import javax.persistence.Table; @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.COMPONENT_DESCRIPTOR_TABLE_NAME) -public class ComponentDescriptorEntity extends BaseSqlEntity implements SearchTextEntity { +public class ComponentDescriptorEntity extends BaseSqlEntity { @Enumerated(EnumType.STRING) @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY) @@ -71,9 +70,6 @@ public class ComponentDescriptorEntity extends BaseSqlEntity implements SearchTextEntity { +public final class CustomerEntity extends BaseSqlEntity { @Column(name = ModelConstants.CUSTOMER_TENANT_ID_PROPERTY) private UUID tenantId; @Column(name = ModelConstants.CUSTOMER_TITLE_PROPERTY) private String title; - - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - + @Column(name = ModelConstants.COUNTRY_PROPERTY) private String country; @@ -105,16 +101,6 @@ public final class CustomerEntity extends BaseSqlEntity implements Sea } } - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public Customer toData() { Customer customer = new Customer(new CustomerId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java index 1e99a59773..a7a92a34e2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; @@ -31,13 +30,11 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import java.io.IOException; import java.util.HashSet; import java.util.UUID; @@ -47,7 +44,7 @@ import java.util.UUID; @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.DASHBOARD_TABLE_NAME) -public final class DashboardEntity extends BaseSqlEntity implements SearchTextEntity { +public final class DashboardEntity extends BaseSqlEntity { private static final JavaType assignedCustomersType = JacksonUtil.constructCollectionType(HashSet.class, ShortCustomerInfo.class); @@ -61,9 +58,6 @@ public final class DashboardEntity extends BaseSqlEntity implements S @Column(name = ModelConstants.DASHBOARD_IMAGE_PROPERTY) private String image; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) private String assignedCustomers; @@ -109,16 +103,6 @@ public final class DashboardEntity extends BaseSqlEntity implements S } } - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public Dashboard toData() { Dashboard dashboard = new Dashboard(new DashboardId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java index d3c8bbeb34..eeb03c8630 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import lombok.Data; import lombok.EqualsAndHashCode; @@ -28,12 +27,10 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import java.io.IOException; import java.util.HashSet; import java.util.UUID; @@ -42,7 +39,7 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.DASHBOARD_TABLE_NAME) -public class DashboardInfoEntity extends BaseSqlEntity implements SearchTextEntity { +public class DashboardInfoEntity extends BaseSqlEntity { private static final JavaType assignedCustomersType = JacksonUtil.constructCollectionType(HashSet.class, ShortCustomerInfo.class); @@ -56,9 +53,6 @@ public class DashboardInfoEntity extends BaseSqlEntity implements @Column(name = ModelConstants.DASHBOARD_IMAGE_PROPERTY) private String image; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) private String assignedCustomers; @@ -93,20 +87,6 @@ public class DashboardInfoEntity extends BaseSqlEntity implements this.mobileOrder = dashboardInfo.getMobileOrder(); } - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - @Override public DashboardInfo toData() { DashboardInfo dashboardInfo = new DashboardInfo(new DashboardId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index 1b084c4817..a063fce9c2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import javax.persistence.Column; @@ -49,7 +48,7 @@ import java.util.UUID; @Entity @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) @Table(name = ModelConstants.DEVICE_PROFILE_TABLE_NAME) -public final class DeviceProfileEntity extends BaseSqlEntity implements SearchTextEntity { +public final class DeviceProfileEntity extends BaseSqlEntity { @Column(name = ModelConstants.DEVICE_PROFILE_TENANT_ID_PROPERTY) private UUID tenantId; @@ -75,9 +74,6 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.DEVICE_PROFILE_DESCRIPTION_PROPERTY) private String description; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.DEVICE_PROFILE_IS_DEFAULT_PROPERTY) private boolean isDefault; @@ -151,20 +147,6 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl } } - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - @Override public DeviceProfile toData() { DeviceProfile deviceProfile = new DeviceProfile(new DeviceProfileId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index dcb3bce546..0cd8f44027 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -61,7 +60,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = OTA_PACKAGE_TABLE_NAME) -public class OtaPackageEntity extends BaseSqlEntity implements SearchTextEntity { +public class OtaPackageEntity extends BaseSqlEntity { @Column(name = OTA_PACKAGE_TENANT_ID_COLUMN) private UUID tenantId; @@ -109,9 +108,6 @@ public class OtaPackageEntity extends BaseSqlEntity implements Searc @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - public OtaPackageEntity() { super(); } @@ -137,16 +133,6 @@ public class OtaPackageEntity extends BaseSqlEntity implements Searc this.additionalInfo = otaPackage.getAdditionalInfo(); } - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public OtaPackage toData() { OtaPackage otaPackage = new OtaPackage(new OtaPackageId(id)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java index 5a2eb6008b..8434fc936f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -60,7 +59,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = OTA_PACKAGE_TABLE_NAME) -public class OtaPackageInfoEntity extends BaseSqlEntity implements SearchTextEntity { +public class OtaPackageInfoEntity extends BaseSqlEntity { @Column(name = OTA_PACKAGE_TENANT_ID_COLUMN) private UUID tenantId; @@ -104,9 +103,6 @@ public class OtaPackageInfoEntity extends BaseSqlEntity implemen @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; - @Column(name = SEARCH_TEXT_PROPERTY) - private String searchText; - @Transient private boolean hasData; @@ -155,16 +151,6 @@ public class OtaPackageInfoEntity extends BaseSqlEntity implemen this.additionalInfo = JacksonUtil.convertValue(additionalInfo, JsonNode.class); } - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public OtaPackageInfo toData() { OtaPackageInfo otaPackageInfo = new OtaPackageInfo(new OtaPackageId(id)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java index 82362e0259..ed8ee01b6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java @@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -43,7 +42,7 @@ import java.util.UUID; @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.RULE_CHAIN_TABLE_NAME) -public class RuleChainEntity extends BaseSqlEntity implements SearchTextEntity { +public class RuleChainEntity extends BaseSqlEntity { @Column(name = ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY) private UUID tenantId; @@ -55,9 +54,6 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT @Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) private RuleChainType type; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) private UUID firstRuleNodeId; @@ -89,7 +85,6 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); this.name = ruleChain.getName(); this.type = ruleChain.getType(); - this.searchText = ruleChain.getName(); if (ruleChain.getFirstRuleNodeId() != null) { this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); } @@ -102,16 +97,6 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT } } - @Override - public String getSearchTextSource() { - return searchText; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public RuleChain toData() { RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java index 7ef1edec19..c984f7b1d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java @@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -39,7 +38,7 @@ import java.util.UUID; @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.RULE_NODE_TABLE_NAME) -public class RuleNodeEntity extends BaseSqlEntity implements SearchTextEntity { +public class RuleNodeEntity extends BaseSqlEntity { @Column(name = ModelConstants.RULE_NODE_CHAIN_ID_PROPERTY) private UUID ruleChainId; @@ -50,9 +49,6 @@ public class RuleNodeEntity extends BaseSqlEntity implements SearchTex @Column(name = ModelConstants.RULE_NODE_NAME_PROPERTY) private String name; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.RULE_NODE_VERSION_PROPERTY) private int configurationVersion; @@ -88,7 +84,6 @@ public class RuleNodeEntity extends BaseSqlEntity implements SearchTex this.name = ruleNode.getName(); this.debugMode = ruleNode.isDebugMode(); this.singletonMode = ruleNode.isSingletonMode(); - this.searchText = ruleNode.getName(); this.configurationVersion = ruleNode.getConfigurationVersion(); this.configuration = ruleNode.getConfiguration(); this.additionalInfo = ruleNode.getAdditionalInfo(); @@ -97,16 +92,6 @@ public class RuleNodeEntity extends BaseSqlEntity implements SearchTex } } - @Override - public String getSearchTextSource() { - return searchText; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public RuleNode toData() { RuleNode ruleNode = new RuleNode(new RuleNodeId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index 1b30123b2e..4fbc50fd69 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -21,8 +21,8 @@ import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; -import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; @@ -31,6 +31,7 @@ import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DATA_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; @@ -42,7 +43,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER @EqualsAndHashCode(callSuper = true) @Entity @Table(name = RESOURCE_TABLE_NAME) -public class TbResourceEntity extends BaseSqlEntity implements SearchTextEntity { +public class TbResourceEntity extends BaseSqlEntity implements BaseEntity { @Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid") private UUID tenantId; @@ -65,6 +66,9 @@ public class TbResourceEntity extends BaseSqlEntity implements Searc @Column(name = RESOURCE_DATA_COLUMN) private String data; + @Column(name = RESOURCE_ETAG_COLUMN) + private String etag; + public TbResourceEntity() { } @@ -82,6 +86,7 @@ public class TbResourceEntity extends BaseSqlEntity implements Searc this.searchText = resource.getSearchText(); this.fileName = resource.getFileName(); this.data = resource.getData(); + this.etag = resource.getEtag(); } @Override @@ -95,11 +100,8 @@ public class TbResourceEntity extends BaseSqlEntity implements Searc resource.setSearchText(searchText); resource.setFileName(fileName); resource.setData(data); + resource.setEtag(etag); return resource; } - @Override - public String getSearchTextSource() { - return this.searchText; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index 5ebc0297af..e8504bc388 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -21,14 +21,15 @@ import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; -import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; @@ -40,7 +41,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER @EqualsAndHashCode(callSuper = true) @Entity @Table(name = RESOURCE_TABLE_NAME) -public class TbResourceInfoEntity extends BaseSqlEntity implements SearchTextEntity { +public class TbResourceInfoEntity extends BaseSqlEntity implements BaseEntity { @Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid") private UUID tenantId; @@ -57,6 +58,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; + @Column(name = RESOURCE_ETAG_COLUMN) + private String hashCode; + public TbResourceInfoEntity() { } @@ -70,6 +74,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen this.resourceType = resource.getResourceType().name(); this.resourceKey = resource.getResourceKey(); this.searchText = resource.getSearchText(); + this.hashCode = resource.getEtag(); } @Override @@ -81,11 +86,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen resource.setResourceType(ResourceType.valueOf(resourceType)); resource.setResourceKey(resourceKey); resource.setSearchText(searchText); + resource.setEtag(hashCode); return resource; } - - @Override - public String getSearchTextSource() { - return title; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java index 9672ed2bab..dce670960c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import javax.persistence.Column; @@ -39,7 +38,7 @@ import javax.persistence.Table; @Entity @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) @Table(name = ModelConstants.TENANT_PROFILE_TABLE_NAME) -public final class TenantProfileEntity extends BaseSqlEntity implements SearchTextEntity { +public final class TenantProfileEntity extends BaseSqlEntity { @Column(name = ModelConstants.TENANT_PROFILE_NAME_PROPERTY) private String name; @@ -47,9 +46,6 @@ public final class TenantProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.TENANT_PROFILE_DESCRIPTION_PROPERTY) private String description; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.TENANT_PROFILE_IS_DEFAULT_PROPERTY) private boolean isDefault; @@ -76,20 +72,6 @@ public final class TenantProfileEntity extends BaseSqlEntity impl this.profileData = JacksonUtil.convertValue(tenantProfile.getProfileData(), ObjectNode.class); } - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - - public String getSearchText() { - return searchText; - } - @Override public TenantProfile toData() { TenantProfile tenantProfile = new TenantProfile(new TenantProfileId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java index 33b19d765e..27083ac93e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; @@ -45,7 +44,7 @@ import java.util.UUID; @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.USER_PG_HIBERNATE_TABLE_NAME) -public class UserEntity extends BaseSqlEntity implements SearchTextEntity { +public class UserEntity extends BaseSqlEntity { @Column(name = ModelConstants.USER_TENANT_ID_PROPERTY) private UUID tenantId; @@ -60,9 +59,6 @@ public class UserEntity extends BaseSqlEntity implements SearchTextEntity< @Column(name = ModelConstants.USER_EMAIL_PROPERTY, unique = true) private String email; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.USER_FIRST_NAME_PROPERTY) private String firstName; @@ -98,16 +94,6 @@ public class UserEntity extends BaseSqlEntity implements SearchTextEntity< this.additionalInfo = user.getAdditionalInfo(); } - @Override - public String getSearchTextSource() { - return email; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public User toData() { User user = new User(new UserId(this.getUuid())); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java index 0bc2f0ba36..fedfa1471d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; @@ -34,7 +33,7 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.WIDGETS_BUNDLE_TABLE_NAME) -public final class WidgetsBundleEntity extends BaseSqlEntity implements SearchTextEntity { +public final class WidgetsBundleEntity extends BaseSqlEntity { @Column(name = ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY) private UUID tenantId; @@ -45,9 +44,6 @@ public final class WidgetsBundleEntity extends BaseSqlEntity impl @Column(name = ModelConstants.WIDGETS_BUNDLE_TITLE_PROPERTY) private String title; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Column(name = ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY) private String image; @@ -78,16 +74,6 @@ public final class WidgetsBundleEntity extends BaseSqlEntity impl } } - @Override - public String getSearchTextSource() { - return title; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - @Override public WidgetsBundle toData() { WidgetsBundle widgetsBundle = new WidgetsBundle(new WidgetsBundleId(id)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index f5ba4abf2c..974e3665f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TbResourceId; @@ -103,17 +104,19 @@ public class BaseResourceService implements ResourceService { } @Override - public PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { + TenantId tenantId = filter.getTenantId(); log.trace("Executing findAllTenantResourcesByTenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return resourceInfoDao.findAllTenantResourcesByTenantId(tenantId.getId(), pageLink); + return resourceInfoDao.findAllTenantResourcesByTenantId(filter, pageLink); } @Override - public PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { + TenantId tenantId = filter.getTenantId(); log.trace("Executing findTenantResourcesByTenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return resourceInfoDao.findTenantResourcesByTenantId(tenantId.getId(), pageLink); + return resourceInfoDao.findTenantResourcesByTenantId(filter, pageLink); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 6a726cd6ec..f0f0b6fdad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; @@ -24,8 +25,8 @@ import java.util.UUID; public interface TbResourceInfoDao extends Dao { - PageData findAllTenantResourcesByTenantId(UUID tenantId, PageLink pageLink); + PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); - PageData findTenantResourcesByTenantId(UUID tenantId, PageLink pageLink); + PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index b6d6bbc0ad..6da6d96637 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.settings; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -58,10 +59,18 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) { log.trace("Executing saveAdminSettings [{}]", adminSettings); adminSettingsValidator.validate(adminSettings, data -> tenantId); - if (adminSettings.getKey().equals("mail") && !adminSettings.getJsonValue().has("password")) { + if (adminSettings.getKey().equals("mail")){ AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail"); if (mailSettings != null) { - ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); + JsonNode newJsonValue = adminSettings.getJsonValue(); + JsonNode oldJsonValue = mailSettings.getJsonValue(); + if (!newJsonValue.has("password") && oldJsonValue.has("password")){ + ((ObjectNode) newJsonValue).put("password", oldJsonValue.get("password").asText()); + } + if (!newJsonValue.has("refreshToken") && oldJsonValue.has("refreshToken")){ + ((ObjectNode) newJsonValue).put("refreshToken", oldJsonValue.get("refreshToken").asText()); + } + dropTokenIfProviderInfoChanged(newJsonValue, oldJsonValue); } } if (adminSettings.getTenantId() == null) { @@ -82,4 +91,18 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { adminSettingsDao.removeByTenantId(tenantId.getId()); } + private void dropTokenIfProviderInfoChanged(JsonNode newJsonValue, JsonNode oldJsonValue) { + if (newJsonValue.has("enableOauth2") && newJsonValue.get("enableOauth2").asBoolean()){ + if (!newJsonValue.get("providerId").equals(oldJsonValue.get("providerId")) || + !newJsonValue.get("clientId").equals(oldJsonValue.get("clientId")) || + !newJsonValue.get("clientSecret").equals(oldJsonValue.get("clientSecret")) || + !newJsonValue.get("redirectUri").equals(oldJsonValue.get("redirectUri")) || + (newJsonValue.has("providerTenantId") && !newJsonValue.get("providerTenantId").equals(oldJsonValue.get("providerTenantId")))){ + ((ObjectNode) newJsonValue).put("tokenGenerated", false); + ((ObjectNode) newJsonValue).remove("refreshToken"); + ((ObjectNode) newJsonValue).remove("refreshTokenExpires"); + } + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractSearchTextDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractSearchTextDao.java deleted file mode 100644 index 6f63c67c54..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractSearchTextDao.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.dao.sql; - -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.SearchTextEntity; - -/** - * Created by Valerii Sosliuk on 5/6/2017. - */ -public abstract class JpaAbstractSearchTextDao , D> extends JpaAbstractDao { - - @Override - protected void setSearchText(E entity) { - ((SearchTextEntity) entity).setSearchText(((SearchTextEntity) entity).getSearchTextSource().toLowerCase()); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index 58bf9f3f0b..b18f80bcd8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -34,14 +34,14 @@ public interface AssetProfileRepository extends JpaRepository findAssetProfiles(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " + "FROM AssetProfileEntity a WHERE " + - "a.tenantId = :tenantId AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "a.tenantId = :tenantId AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findAssetProfileInfos(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index 508064ded2..b14f346cd4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -41,7 +41,7 @@ public interface AssetRepository extends JpaRepository, Expor AssetInfoEntity findAssetInfoById(@Param("assetId") UUID assetId); @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -51,17 +51,17 @@ public interface AssetRepository extends JpaRepository, Expor "LEFT JOIN CustomerEntity c on c.id = a.customerId " + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + - "AND (LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "AND (LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + "OR LOWER(a.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + - "OR LOWER(p.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + - "OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))") + "OR LOWER(p.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(c.title) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findAssetInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -69,7 +69,7 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.assetProfileId = :profileId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, @Param("profileId") UUID profileId, @Param("searchText") String searchText, @@ -81,7 +81,7 @@ public interface AssetRepository extends JpaRepository, Expor "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -95,7 +95,7 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -107,9 +107,9 @@ public interface AssetRepository extends JpaRepository, Expor "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + "AND a.type = :type " + - "AND (LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "AND (LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + "OR LOWER(a.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + - "OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))") + "OR LOWER(c.title) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findAssetInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -121,9 +121,9 @@ public interface AssetRepository extends JpaRepository, Expor "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + "AND a.assetProfileId = :assetProfileId " + - "AND (LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "AND (LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + "OR LOWER(a.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + - "OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))") + "OR LOWER(c.title) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findAssetInfosByTenantIdAndAssetProfileId(@Param("tenantId") UUID tenantId, @Param("assetProfileId") UUID assetProfileId, @Param("textSearch") String textSearch, @@ -132,7 +132,7 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -146,7 +146,7 @@ public interface AssetRepository extends JpaRepository, Expor "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findAssetInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -160,7 +160,7 @@ public interface AssetRepository extends JpaRepository, Expor "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND a.assetProfileId = :assetProfileId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("assetProfileId") UUID assetProfileId, @@ -175,7 +175,7 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -185,7 +185,7 @@ public interface AssetRepository extends JpaRepository, Expor "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + "AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index ed6e79f5dd..ec5a8318f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -34,7 +34,7 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; @@ -53,7 +53,7 @@ import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE @Component @SqlDao @Slf4j -public class JpaAssetDao extends JpaAbstractSearchTextDao implements AssetDao { +public class JpaAssetDao extends JpaAbstractDao implements AssetDao { @Autowired private AssetRepository assetRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index 679fc79f06..fb709b56dc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -29,14 +29,14 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.model.sql.AssetProfileEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import java.util.Objects; import java.util.Optional; import java.util.UUID; @Component -public class JpaAssetProfileDao extends JpaAbstractSearchTextDao implements AssetProfileDao { +public class JpaAssetProfileDao extends JpaAbstractDao implements AssetProfileDao { @Autowired private AssetProfileRepository assetProfileRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java index a6a751a37b..ad8a07be08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java @@ -76,7 +76,6 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com .setParameter("configuration_version", entity.getConfigurationVersion()) .setParameter("name", entity.getName()) .setParameter("scope", entity.getScope().name()) - .setParameter("search_text", entity.getSearchText()) .setParameter("type", entity.getType().name()) .setParameter("clustering_mode", entity.getClusteringMode().name()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java index db2939f449..aef31602fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java @@ -36,13 +36,13 @@ public interface ComponentDescriptorRepository extends JpaRepository findByType(@Param("type") ComponentType type, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT cd FROM ComponentDescriptorEntity cd WHERE cd.type = :type " + - "AND cd.scope = :scope AND LOWER(cd.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND cd.scope = :scope AND LOWER(cd.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByScopeAndType(@Param("type") ComponentType type, @Param("scope") ComponentScope scope, @Param("textSearch") String textSearch, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java index cc1c047c05..58a6c784cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.component.ComponentDescriptorDao; import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import java.util.Objects; import java.util.Optional; @@ -40,7 +40,7 @@ import java.util.UUID; * Created by Valerii Sosliuk on 5/6/2017. */ @Component -public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao +public class JpaBaseComponentDescriptorDao extends JpaAbstractDao implements ComponentDescriptorDao { @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java index 0ba01340c2..c462f263f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java @@ -44,10 +44,10 @@ public class SqlComponentDescriptorInsertRepository extends AbstractComponentDes } private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) { - return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, configuration_version, name, scope, search_text, type, clustering_mode) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :configuration_version, :name, :scope, :search_text, :type, :clustering_mode) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *"; + return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, configuration_version, name, scope, type, clustering_mode) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :configuration_version, :name, :scope, :type, :clustering_mode) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *"; } private static String getUpdateStatement(String id) { - return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, configuration_version = :configuration_version, name = :name, scope = :scope, search_text = :search_text, type = :type, clustering_mode = :clustering_mode"; + return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, configuration_version = :configuration_version, name = :name, scope = :scope, type = :type, clustering_mode = :clustering_mode"; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java index 2f6686f82d..22443ec461 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java @@ -31,7 +31,7 @@ import java.util.UUID; public interface CustomerRepository extends JpaRepository, ExportableEntityRepository { @Query("SELECT c FROM CustomerEntity c WHERE c.tenantId = :tenantId " + - "AND LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(c.title) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index f4d5ae824d..a10ea6765c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.model.sql.CustomerEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Objects; @@ -39,7 +39,7 @@ import java.util.UUID; */ @Component @SqlDao -public class JpaCustomerDao extends JpaAbstractSearchTextDao implements CustomerDao { +public class JpaCustomerDao extends JpaAbstractDao implements CustomerDao { @Autowired private CustomerRepository customerRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 735cffa5f7..d8b07e9364 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -32,14 +32,14 @@ public interface DashboardInfoRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT di FROM DashboardInfoEntity di WHERE di.tenantId = :tenantId " + "AND di.mobileHide = false " + - "AND LOWER(di.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(di.title) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findMobileByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @@ -47,7 +47,7 @@ public interface DashboardInfoRepository extends JpaRepository findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -57,7 +57,7 @@ public interface DashboardInfoRepository extends JpaRepository findMobileByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -66,7 +66,7 @@ public interface DashboardInfoRepository extends JpaRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java index 5d7418a293..35e746bc05 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dashboard.DashboardDao; import org.thingsboard.server.dao.model.sql.DashboardEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; @@ -39,7 +39,7 @@ import java.util.UUID; */ @Component @SqlDao -public class JpaDashboardDao extends JpaAbstractSearchTextDao implements DashboardDao { +public class JpaDashboardDao extends JpaAbstractDao implements DashboardDao { @Autowired DashboardRepository dashboardRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index be69efd39f..2e2148f089 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; @@ -40,7 +40,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao implements DashboardInfoDao { +public class JpaDashboardInfoDao extends JpaAbstractDao implements DashboardInfoDao { @Autowired private DashboardInfoRepository dashboardInfoRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 47b4b4ba79..cfd8b5bb6a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -35,21 +35,21 @@ public interface DeviceProfileRepository extends JpaRepository findDeviceProfiles(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + "FROM DeviceProfileEntity d WHERE " + - "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "d.tenantId = :tenantId AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + "FROM DeviceProfileEntity d WHERE " + - "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, @Param("transportType") DeviceTransportType transportType, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index f2338d1e7b..f500600722 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Objects; @@ -40,7 +40,7 @@ import java.util.UUID; @Component @SqlDao -public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao implements DeviceProfileDao { +public class JpaDeviceProfileDao extends JpaAbstractDao implements DeviceProfileDao { @Autowired private DeviceProfileRepository deviceProfileRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java index b1935aae13..83816fec3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -30,7 +30,7 @@ public interface EdgeRepository extends JpaRepository { @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -43,7 +43,7 @@ public interface EdgeRepository extends JpaRepository { EdgeInfoEntity findEdgeInfoById(@Param("edgeId") UUID edgeId); @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -52,14 +52,14 @@ public interface EdgeRepository extends JpaRepository { "FROM EdgeEntity d " + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + "WHERE d.tenantId = :tenantId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findEdgeInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -70,7 +70,7 @@ public interface EdgeRepository extends JpaRepository { "LEFT JOIN CustomerEntity c on c.id = d.customerId " + "WHERE d.tenantId = :tenantId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findEdgeInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -79,7 +79,7 @@ public interface EdgeRepository extends JpaRepository { @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -91,7 +91,7 @@ public interface EdgeRepository extends JpaRepository { "LEFT JOIN CustomerEntity c on c.id = a.customerId " + "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findEdgeInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -103,7 +103,7 @@ public interface EdgeRepository extends JpaRepository { "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(a.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findEdgeInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -113,7 +113,7 @@ public interface EdgeRepository extends JpaRepository { @Query("SELECT ee FROM EdgeEntity ee, RelationEntity re WHERE ee.tenantId = :tenantId " + "AND ee.id = re.fromId AND re.fromType = 'EDGE' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.toId = :entityId AND re.toType = :entityType " + - "AND LOWER(ee.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(ee.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndEntityId(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId, @Param("entityType") String entityType, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index 239fe1f098..bb825f504d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -34,7 +34,7 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.edge.EdgeEventDao; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.EdgeEventEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; @@ -55,7 +55,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @SqlDao @RequiredArgsConstructor @Slf4j -public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao implements EdgeEventDao { +public class JpaBaseEdgeEventDao extends JpaAbstractDao implements EdgeEventDao { private final UUID systemTenantId = NULL_UUID; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java index 7ddd3ba590..c40ca2ca6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -31,7 +31,7 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.model.sql.EdgeEntity; import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; @@ -44,7 +44,7 @@ import java.util.UUID; @Component @Slf4j @SqlDao -public class JpaEdgeDao extends JpaAbstractSearchTextDao implements EdgeDao { +public class JpaEdgeDao extends JpaAbstractDao implements EdgeDao { @Autowired private EdgeRepository edgeRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index 66f52ccbf7..1478e23a54 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -39,7 +39,7 @@ public interface EntityViewRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -48,14 +48,14 @@ public interface EntityViewRepository extends JpaRepository findEntityViewInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + "AND e.type = :type " + - "AND LOWER(e.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(e.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -66,7 +66,7 @@ public interface EntityViewRepository extends JpaRepository findEntityViewInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -74,7 +74,7 @@ public interface EntityViewRepository extends JpaRepository findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -85,7 +85,7 @@ public interface EntityViewRepository extends JpaRepository findEntityViewInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -94,7 +94,7 @@ public interface EntityViewRepository extends JpaRepository findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -107,7 +107,7 @@ public interface EntityViewRepository extends JpaRepository findEntityViewInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -124,7 +124,7 @@ public interface EntityViewRepository extends JpaRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -134,7 +134,7 @@ public interface EntityViewRepository extends JpaRepository findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 01df262b01..f3f82d34da 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -32,7 +32,7 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.entityview.EntityViewDao; import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; @@ -48,7 +48,7 @@ import java.util.UUID; @Component @Slf4j @SqlDao -public class JpaEntityViewDao extends JpaAbstractSearchTextDao +public class JpaEntityViewDao extends JpaAbstractDao implements EntityViewDao { @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index d2b408e92e..cc2675640d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.sql.OtaPackageEntity; import org.thingsboard.server.dao.ota.OtaPackageDao; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.UUID; @@ -32,7 +32,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaOtaPackageDao extends JpaAbstractSearchTextDao implements OtaPackageDao { +public class JpaOtaPackageDao extends JpaAbstractDao implements OtaPackageDao { @Autowired private OtaPackageRepository otaPackageRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java index 0fb6d506c2..f50dd6ed0f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity; import org.thingsboard.server.dao.ota.OtaPackageInfoDao; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Objects; @@ -38,7 +38,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaOtaPackageInfoDao extends JpaAbstractSearchTextDao implements OtaPackageInfoDao { +public class JpaOtaPackageInfoDao extends JpaAbstractDao implements OtaPackageInfoDao { @Autowired private OtaPackageInfoRepository otaPackageInfoRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java index eb9b0253bb..3d81f1b567 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java @@ -28,7 +28,7 @@ import java.util.UUID; public interface OtaPackageInfoRepository extends JpaRepository { @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + - "AND LOWER(f.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(f.title) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findAllByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @@ -38,7 +38,7 @@ public interface OtaPackageInfoRepository extends JpaRepository findAllByTenantIdAndTypeAndDeviceProfileIdAndHasData(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, @Param("type") OtaPackageType type, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index 781f1a9276..9b6ef62501 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.TbResourceEntity; import org.thingsboard.server.dao.resource.TbResourceDao; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; @@ -37,7 +37,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaTbResourceDao extends JpaAbstractSearchTextDao implements TbResourceDao { +public class JpaTbResourceDao extends JpaAbstractDao implements TbResourceDao { private final TbResourceRepository resourceRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java index 837ab63fb3..99b30f0971 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java @@ -19,14 +19,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity; import org.thingsboard.server.dao.resource.TbResourceInfoDao; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Objects; @@ -35,7 +37,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaTbResourceInfoDao extends JpaAbstractSearchTextDao implements TbResourceInfoDao { +public class JpaTbResourceInfoDao extends JpaAbstractDao implements TbResourceInfoDao { @Autowired private TbResourceInfoRepository resourceInfoRepository; @@ -51,21 +53,26 @@ public class JpaTbResourceInfoDao extends JpaAbstractSearchTextDao findAllTenantResourcesByTenantId(UUID tenantId, PageLink pageLink) { + public PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { + ResourceType resourceType = filter.getResourceType(); return DaoUtil.toPageData(resourceInfoRepository .findAllTenantResourcesByTenantId( - tenantId, + filter.getTenantId().getId(), TenantId.NULL_UUID, + resourceType == null ? null : resourceType.name(), Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); } @Override - public PageData findTenantResourcesByTenantId(UUID tenantId, PageLink pageLink) { + public PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { + ResourceType resourceType = filter.getResourceType(); return DaoUtil.toPageData(resourceInfoRepository .findTenantResourcesByTenantId( - tenantId, + filter.getTenantId().getId(), + resourceType == null ? null : resourceType.name(), Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index dde66910df..720b78a5f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -34,16 +34,21 @@ public interface TbResourceInfoRepository extends JpaRepository findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, @Param("systemAdminId") UUID sysadminId, + @Param("resourceType") String resourceType, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " + "ri.tenantId = :tenantId " + + "AND (:resourceType IS NULL OR ri.resourceType = :resourceType)" + "AND LOWER(ri.title) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, + @Param("resourceType") String resourceType, @Param("searchText") String searchText, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index fb9c566cb3..7b5e6b8d79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.RuleChainEntity; import org.thingsboard.server.dao.rule.RuleChainDao; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Collection; @@ -40,7 +40,7 @@ import java.util.UUID; @Slf4j @Component @SqlDao -public class JpaRuleChainDao extends JpaAbstractSearchTextDao implements RuleChainDao { +public class JpaRuleChainDao extends JpaAbstractDao implements RuleChainDao { @Autowired private RuleChainRepository ruleChainRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index 58cf179fea..7adf8aa0c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.RuleNodeEntity; import org.thingsboard.server.dao.rule.RuleNodeDao; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; @@ -40,7 +40,7 @@ import java.util.stream.Collectors; @Slf4j @Component @SqlDao -public class JpaRuleNodeDao extends JpaAbstractSearchTextDao implements RuleNodeDao { +public class JpaRuleNodeDao extends JpaAbstractDao implements RuleNodeDao { @Autowired private RuleNodeRepository ruleNodeRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index 2482a606b2..8cc57c62ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -30,14 +30,14 @@ import java.util.UUID; public interface RuleChainRepository extends JpaRepository, ExportableEntityRepository { @Query("SELECT rc FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId " + - "AND LOWER(rc.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(rc.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT rc FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId " + "AND rc.type = :type " + - "AND LOWER(rc.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(rc.name) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") RuleChainType type, @Param("searchText") String searchText, @@ -46,7 +46,7 @@ public interface RuleChainRepository extends JpaRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -55,7 +55,7 @@ public interface RuleChainRepository extends JpaRepository findAutoAssignByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index b9715c5a7a..9f01494e5c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.TenantEntity; import org.thingsboard.server.dao.model.sql.TenantInfoEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.util.SqlDao; @@ -43,7 +43,7 @@ import java.util.stream.Collectors; */ @Component @SqlDao -public class JpaTenantDao extends JpaAbstractSearchTextDao implements TenantDao { +public class JpaTenantDao extends JpaAbstractDao implements TenantDao { @Autowired private TenantRepository tenantRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java index 6ba2337f5f..2d65326b50 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantProfileDao.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.TenantProfileEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.tenant.TenantProfileDao; import org.thingsboard.server.dao.util.SqlDao; @@ -36,7 +36,7 @@ import java.util.UUID; @Component @SqlDao -public class JpaTenantProfileDao extends JpaAbstractSearchTextDao implements TenantProfileDao { +public class JpaTenantProfileDao extends JpaAbstractDao implements TenantProfileDao { @Autowired private TenantProfileRepository tenantProfileRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java index 74cc24be86..a42dc1c797 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java @@ -34,13 +34,13 @@ public interface TenantProfileRepository extends JpaRepository findTenantProfiles(@Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + "FROM TenantProfileEntity t " + - "WHERE LOWER(t.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "WHERE LOWER(t.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findTenantProfileInfos(@Param("textSearch") String textSearch, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 6e3b35ed0c..5174e3af83 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -37,14 +37,14 @@ public interface TenantRepository extends JpaRepository { "WHERE t.id = :tenantId") TenantInfoEntity findTenantInfoById(@Param("tenantId") UUID tenantId); - @Query("SELECT t FROM TenantEntity t WHERE LOWER(t.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + @Query("SELECT t FROM TenantEntity t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findTenantsNextPage(@Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.dao.model.sql.TenantInfoEntity(t, p.name) " + "FROM TenantEntity t " + "LEFT JOIN TenantProfileEntity p on p.id = t.tenantProfileId " + - "WHERE LOWER(t.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findTenantInfosNextPage(@Param("textSearch") String textSearch, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 04b919b484..e1282f7150 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.UserEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.user.UserDao; import org.thingsboard.server.dao.util.SqlDao; @@ -43,7 +43,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; */ @Component @SqlDao -public class JpaUserDao extends JpaAbstractSearchTextDao implements UserDao { +public class JpaUserDao extends JpaAbstractDao implements UserDao { @Autowired private UserRepository userRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 2f6866d45b..c24c66fc7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -37,7 +37,7 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + "AND u.customerId = :customerId AND u.authority = :authority " + - "AND LOWER(u.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(u.email) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findUsersByAuthority(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -46,14 +46,14 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + "AND u.customerId IN (:customerIds) " + - "AND LOWER(u.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(u.email) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findTenantAndCustomerUsers(@Param("tenantId") UUID tenantId, @Param("customerIds") Collection customerIds, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + - "AND LOWER(u.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND LOWER(u.email) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index d9589565c9..f3a476d8eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; @@ -41,7 +41,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; */ @Component @SqlDao -public class JpaWidgetsBundleDao extends JpaAbstractSearchTextDao implements WidgetsBundleDao { +public class JpaWidgetsBundleDao extends JpaAbstractDao implements WidgetsBundleDao { @Autowired private WidgetsBundleRepository widgetsBundleRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index bb9f33cd3d..7a6ba68a36 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -33,19 +33,19 @@ public interface WidgetsBundleRepository extends JpaRepository findSystemWidgetsBundles(@Param("systemTenantId") UUID systemTenantId, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :tenantId " + - "AND LOWER(wb.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(wb.title) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId IN (:tenantId, :nullTenantId) " + - "AND LOWER(wb.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND LOWER(wb.title) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findAllTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId, @Param("nullTenantId") UUID nullTenantId, @Param("textSearch") String textSearch, diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index af0d5c116f..7fe3ec6e67 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -125,7 +125,6 @@ CREATE TABLE IF NOT EXISTS component_descriptor ( configuration_version int DEFAULT 0, name varchar(255), scope varchar(255), - search_text varchar(255), type varchar(255), clustering_mode varchar(255) ); @@ -140,7 +139,6 @@ CREATE TABLE IF NOT EXISTS customer ( country varchar(255), email varchar(255), phone varchar(255), - search_text varchar(255), state varchar(255), tenant_id uuid, title varchar(255), @@ -154,7 +152,6 @@ CREATE TABLE IF NOT EXISTS dashboard ( created_time bigint NOT NULL, configuration varchar, assigned_customers varchar(1000000), - search_text varchar(255), tenant_id uuid, title varchar(255), mobile_hide boolean DEFAULT false, @@ -174,7 +171,6 @@ CREATE TABLE IF NOT EXISTS rule_chain ( first_rule_node_id uuid, root boolean, debug_mode boolean, - search_text varchar(255), tenant_id uuid, external_id uuid, CONSTRAINT rule_chain_external_id_unq_key UNIQUE (tenant_id, external_id) @@ -191,7 +187,6 @@ CREATE TABLE IF NOT EXISTS rule_node ( name varchar(255), debug_mode boolean, singleton_mode boolean, - search_text varchar(255), external_id uuid ); @@ -223,7 +218,6 @@ CREATE TABLE IF NOT EXISTS ota_package ( data oid, data_size bigint, additional_info varchar, - search_text varchar(255), CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version) ); @@ -248,7 +242,6 @@ CREATE TABLE IF NOT EXISTS asset_profile ( name varchar(255), image varchar(1000000), description varchar, - search_text varchar(255), is_default boolean, tenant_id uuid, default_rule_chain_id uuid, @@ -271,7 +264,6 @@ CREATE TABLE IF NOT EXISTS asset ( asset_profile_id uuid NOT NULL, name varchar(255), label varchar(255), - search_text varchar(255), tenant_id uuid, type varchar(255), external_id uuid, @@ -290,7 +282,6 @@ CREATE TABLE IF NOT EXISTS device_profile ( provision_type varchar(255), profile_data jsonb, description varchar, - search_text varchar(255), is_default boolean, tenant_id uuid, firmware_id uuid, @@ -444,7 +435,6 @@ CREATE TABLE IF NOT EXISTS tb_user ( first_name varchar(255), last_name varchar(255), phone varchar(255), - search_text varchar(255), tenant_id uuid ); @@ -454,7 +444,6 @@ CREATE TABLE IF NOT EXISTS tenant_profile ( name varchar(255), profile_data jsonb, description varchar, - search_text varchar(255), is_default boolean, isolated_tb_core boolean, isolated_tb_rule_engine boolean, @@ -473,7 +462,6 @@ CREATE TABLE IF NOT EXISTS tenant ( email varchar(255), phone varchar(255), region varchar(255), - search_text varchar(255), state varchar(255), title varchar(255), zip varchar(255), @@ -507,7 +495,6 @@ CREATE TABLE IF NOT EXISTS widgets_bundle ( id uuid NOT NULL CONSTRAINT widgets_bundle_pkey PRIMARY KEY, created_time bigint NOT NULL, alias varchar(255), - search_text varchar(255), tenant_id uuid, title varchar(255), image varchar(1000000), @@ -528,7 +515,6 @@ CREATE TABLE IF NOT EXISTS entity_view ( keys varchar(10000000), start_ts bigint, end_ts bigint, - search_text varchar(255), additional_info varchar, external_id uuid, CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id) @@ -713,6 +699,7 @@ CREATE TABLE IF NOT EXISTS resource ( search_text varchar(255), file_name varchar(255) NOT NULL, data varchar, + etag varchar, CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); @@ -727,7 +714,6 @@ CREATE TABLE IF NOT EXISTS edge ( label varchar(255), routing_key varchar(255), secret varchar(255), - search_text varchar(255), tenant_id uuid, CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), CONSTRAINT edge_routing_key_unq_key UNIQUE (routing_key) diff --git a/dao/src/main/resources/sql/schema-views-and-functions.sql b/dao/src/main/resources/sql/schema-views-and-functions.sql index eaa09d78cc..3cd20ac0c4 100644 --- a/dao/src/main/resources/sql/schema-views-and-functions.sql +++ b/dao/src/main/resources/sql/schema-views-and-functions.sql @@ -23,7 +23,7 @@ SELECT d.* , COALESCE(da.bool_v, FALSE) as active FROM device d LEFT JOIN customer c ON c.id = d.customer_id - LEFT JOIN attribute_kv da ON da.entity_id = d.id and da.attribute_key = 'active'; + LEFT JOIN attribute_kv da ON da.entity_type = 'DEVICE' AND da.entity_id = d.id AND da.attribute_type = 'SERVER_SCOPE' AND da.attribute_key = 'active'; DROP VIEW IF EXISTS device_info_active_ts_view CASCADE; CREATE OR REPLACE VIEW device_info_active_ts_view AS diff --git a/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java index 7ed0c811b2..8b4b650766 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java +++ b/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java @@ -36,7 +36,7 @@ public class TimescaleSqlInitializer { "sql/schema-views-and-functions.sql", "sql/system-data.sql", "sql/system-test-psql.sql"); - private static final String dropAllTablesSqlFile = "sql/timescale/drop-all-tables.sql"; + private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql"; public static void initDb(Connection conn) { cleanUpDb(conn); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index eb6feee500..adb97196fd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -670,7 +670,7 @@ public class AssetServiceTest extends AbstractServiceTest { ) ); - PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getTitle()); List assetInfosWithCustomer = assetService .findAssetInfosByTenantId(tenantId, pageLinkWithCustomer).getData(); @@ -730,7 +730,7 @@ public class AssetServiceTest extends AbstractServiceTest { ) ); - PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getTitle()); List assetInfosWithCustomer = assetService .findAssetInfosByTenantIdAndType(tenantId, asset.getType(), pageLinkWithCustomer).getData(); @@ -776,7 +776,7 @@ public class AssetServiceTest extends AbstractServiceTest { ) ); - PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getTitle()); List assetInfosWithCustomer = assetService .findAssetInfosByTenantIdAndAssetProfileId(tenantId, savedAsset.getAssetProfileId(), pageLinkWithCustomer).getData(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index b35d97f730..b19af16015 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -862,7 +862,7 @@ public class DeviceServiceTest extends AbstractServiceTest { ) ); - PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getTitle()); List deviceInfosWithCustomer = deviceService .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithCustomer).getData(); @@ -922,7 +922,7 @@ public class DeviceServiceTest extends AbstractServiceTest { ) ); - PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getTitle()); List deviceInfosWithCustomer = deviceService .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).type(device.getType()).build(), pageLinkWithCustomer).getData(); @@ -968,7 +968,7 @@ public class DeviceServiceTest extends AbstractServiceTest { ) ); - PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getTitle()); List deviceInfosWithCustomer = deviceService .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).deviceProfileId(savedDevice.getDeviceProfileId()).build(), pageLinkWithCustomer).getData(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java index 2c7674c81b..5872a5846b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; @@ -505,8 +506,11 @@ public class TenantServiceTest extends AbstractServiceTest { assertThat(resourceService.findResourceById(tenant.getId(), resource.getId())) .as("resource").isNull(); PageLink pageLinkResources = new PageLink(1); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(tenantId) + .build(); PageData tenantResources = - resourceService.findAllTenantResourcesByTenantId(tenant.getId(), pageLinkResources); + resourceService.findAllTenantResourcesByTenantId(filter, pageLinkResources); Assert.assertEquals(0, tenantResources.getTotalElements()); } diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql deleted file mode 100644 index 3ad3dac3b3..0000000000 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ /dev/null @@ -1,42 +0,0 @@ -DROP TABLE IF EXISTS admin_settings; -DROP TABLE IF EXISTS entity_alarm; -DROP TABLE IF EXISTS alarm; -DROP TABLE IF EXISTS asset; -DROP TABLE IF EXISTS audit_log; -DROP TABLE IF EXISTS attribute_kv; -DROP TABLE IF EXISTS component_descriptor; -DROP TABLE IF EXISTS customer; -DROP TABLE IF EXISTS device; -DROP TABLE IF EXISTS device_credentials; -DROP TABLE IF EXISTS event; -DROP TABLE IF EXISTS relation; -DROP TABLE IF EXISTS tb_user; -DROP TABLE IF EXISTS tenant; -DROP TABLE IF EXISTS ts_kv; -DROP TABLE IF EXISTS ts_kv_dictionary; -DROP TABLE IF EXISTS ts_kv_latest; -DROP TABLE IF EXISTS user_credentials; -DROP TABLE IF EXISTS widget_type; -DROP TABLE IF EXISTS widgets_bundle; -DROP TABLE IF EXISTS entity_view; -DROP TABLE IF EXISTS device_profile; -DROP TABLE IF EXISTS tenant_profile; -DROP TABLE IF EXISTS dashboard; -DROP TABLE IF EXISTS rule_node_state; -DROP TABLE IF EXISTS rule_node; -DROP TABLE IF EXISTS rule_chain; -DROP TABLE IF EXISTS oauth2_mobile; -DROP TABLE IF EXISTS oauth2_domain; -DROP TABLE IF EXISTS oauth2_registration; -DROP TABLE IF EXISTS oauth2_params; -DROP TABLE IF EXISTS oauth2_client_registration_template; -DROP TABLE IF EXISTS oauth2_client_registration; -DROP TABLE IF EXISTS oauth2_client_registration_info; -DROP TABLE IF EXISTS api_usage_state; -DROP TABLE IF EXISTS resource; -DROP TABLE IF EXISTS ota_package; -DROP TABLE IF EXISTS edge; -DROP TABLE IF EXISTS edge_event; -DROP TABLE IF EXISTS rpc; -DROP TABLE IF EXISTS queue; -DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 2116bc367d..2fca79e01e 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -1,5 +1,23 @@ +DROP FUNCTION IF EXISTS to_uuid; +DROP FUNCTION IF EXISTS create_or_update_active_alarm; +DROP FUNCTION IF EXISTS update_alarm; +DROP FUNCTION IF EXISTS acknowledge_alarm; +DROP FUNCTION IF EXISTS clear_alarm; +DROP FUNCTION IF EXISTS assign_alarm; +DROP FUNCTION IF EXISTS unassign_alarm; + +DROP PROCEDURE IF EXISTS cleanup_edge_events_by_ttl; +DROP PROCEDURE IF EXISTS cleanup_timeseries_by_ttl; +DROP FUNCTION IF EXISTS delete_customer_records_from_ts_kv; + +DROP VIEW IF EXISTS device_info_active_attribute_view CASCADE; +DROP VIEW IF EXISTS device_info_active_ts_view CASCADE; +DROP VIEW IF EXISTS device_info_view CASCADE; +DROP VIEW IF EXISTS alarm_info CASCADE; + DROP TABLE IF EXISTS admin_settings; DROP TABLE IF EXISTS entity_alarm; +DROP TABLE IF EXISTS alarm_comment; DROP TABLE IF EXISTS alarm; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; @@ -8,9 +26,12 @@ DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; DROP TABLE IF EXISTS device; DROP TABLE IF EXISTS device_credentials; -DROP TABLE IF EXISTS event; +DROP TABLE IF EXISTS rule_node_debug_event; +DROP TABLE IF EXISTS rule_chain_debug_event; +DROP TABLE IF EXISTS stats_event; +DROP TABLE IF EXISTS lc_event; +DROP TABLE IF EXISTS error_event; DROP TABLE IF EXISTS relation; -DROP TABLE IF EXISTS tb_user; DROP TABLE IF EXISTS tenant; DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv_latest; @@ -31,13 +52,22 @@ DROP TABLE IF EXISTS oauth2_mobile; DROP TABLE IF EXISTS oauth2_domain; DROP TABLE IF EXISTS oauth2_registration; DROP TABLE IF EXISTS oauth2_params; -DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; +DROP TABLE IF EXISTS oauth2_client_registration_template; +DROP TABLE IF EXISTS ota_package; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; DROP TABLE IF EXISTS firmware; DROP TABLE IF EXISTS edge; DROP TABLE IF EXISTS edge_event; DROP TABLE IF EXISTS rpc; -DROP TABLE IF EXISTS queue; \ No newline at end of file +DROP TABLE IF EXISTS queue; +DROP TABLE IF EXISTS notification; +DROP TABLE IF EXISTS notification_request; +DROP TABLE IF EXISTS notification_rule; +DROP TABLE IF EXISTS notification_template; +DROP TABLE IF EXISTS notification_target; +DROP TABLE IF EXISTS user_settings; +DROP TABLE IF EXISTS user_auth_settings; +DROP TABLE IF EXISTS tb_user; \ No newline at end of file diff --git a/dao/src/test/resources/sql/system-data.sql b/dao/src/test/resources/sql/system-data.sql index bfa33c2284..debf698623 100644 --- a/dao/src/test/resources/sql/system-data.sql +++ b/dao/src/test/resources/sql/system-data.sql @@ -17,9 +17,8 @@ /** SYSTEM **/ /** System admin **/ -INSERT INTO tb_user ( id, created_time, tenant_id, customer_id, email, search_text, authority ) -VALUES ( '5a797660-4612-11e7-a919-92ebcb67fe33', 1592576748000, '13814000-1dd2-11b2-8080-808080808080', '13814000-1dd2-11b2-8080-808080808080', 'sysadmin@thingsboard.org', - 'sysadmin@thingsboard.org', 'SYS_ADMIN' ); +INSERT INTO tb_user ( id, created_time, tenant_id, customer_id, email, authority ) +VALUES ( '5a797660-4612-11e7-a919-92ebcb67fe33', 1592576748000, '13814000-1dd2-11b2-8080-808080808080', '13814000-1dd2-11b2-8080-808080808080', 'sysadmin@thingsboard.org', 'SYS_ADMIN' ); INSERT INTO user_credentials ( id, created_time, user_id, enabled, password ) VALUES ( '61441950-4612-11e7-a919-92ebcb67fe33', 1592576748000, '5a797660-4612-11e7-a919-92ebcb67fe33', true, diff --git a/dao/src/test/resources/sql/system-test-psql.sql b/dao/src/test/resources/sql/system-test-psql.sql index 8d3f08a32f..172731b9c5 100644 --- a/dao/src/test/resources/sql/system-test-psql.sql +++ b/dao/src/test/resources/sql/system-test-psql.sql @@ -1,2 +1,2 @@ --PostgreSQL specific truncate to fit constraints -TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain; \ No newline at end of file +TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain, alarm_comment, alarm, entity_alarm; \ No newline at end of file diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql deleted file mode 100644 index 80330a5ef5..0000000000 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ /dev/null @@ -1,38 +0,0 @@ -DROP TABLE IF EXISTS admin_settings; -DROP TABLE IF EXISTS entity_alarm; -DROP TABLE IF EXISTS alarm; -DROP TABLE IF EXISTS asset; -DROP TABLE IF EXISTS audit_log; -DROP TABLE IF EXISTS attribute_kv; -DROP TABLE IF EXISTS component_descriptor; -DROP TABLE IF EXISTS customer; -DROP TABLE IF EXISTS device; -DROP TABLE IF EXISTS device_credentials; -DROP TABLE IF EXISTS event; -DROP TABLE IF EXISTS relation; -DROP TABLE IF EXISTS tb_user; -DROP TABLE IF EXISTS tenant; -DROP TABLE IF EXISTS ts_kv; -DROP TABLE IF EXISTS ts_kv_latest; -DROP TABLE IF EXISTS ts_kv_dictionary; -DROP TABLE IF EXISTS user_credentials; -DROP TABLE IF EXISTS widget_type; -DROP TABLE IF EXISTS widgets_bundle; -DROP TABLE IF EXISTS rule_node_state; -DROP TABLE IF EXISTS rule_node; -DROP TABLE IF EXISTS rule_chain; -DROP TABLE IF EXISTS entity_view; -DROP TABLE IF EXISTS device_profile; -DROP TABLE IF EXISTS tenant_profile; -DROP TABLE IF EXISTS asset_profile; -DROP TABLE IF EXISTS dashboard; -DROP TABLE IF EXISTS edge; -DROP TABLE IF EXISTS edge_event; -DROP TABLE IF EXISTS tb_schema_settings; -DROP TABLE IF EXISTS oauth2_client_registration; -DROP TABLE IF EXISTS oauth2_client_registration_info; -DROP TABLE IF EXISTS oauth2_client_registration_template; -DROP TABLE IF EXISTS api_usage_state; -DROP TABLE IF EXISTS resource; -DROP TABLE IF EXISTS firmware; -DROP TABLE IF EXISTS queue; diff --git a/docker/.env b/docker/.env index f33ed50da5..37c9768296 100644 --- a/docker/.env +++ b/docker/.env @@ -1,6 +1,6 @@ TB_QUEUE_TYPE=kafka -# redis or redis-cluster +# redis or redis-cluster or redis-sentinel CACHE=redis DOCKER_REPO=thingsboard diff --git a/docker/.gitignore b/docker/.gitignore index c9172ae6ce..b6c9fcf18c 100644 --- a/docker/.gitignore +++ b/docker/.gitignore @@ -12,6 +12,9 @@ tb-node/redis-cluster-data-2/** tb-node/redis-cluster-data-3/** tb-node/redis-cluster-data-4/** tb-node/redis-cluster-data-5/** +tb-node/redis-sentinel-data-master/** +tb-node/redis-sentinel-data-slave/** +tb-node/redis-sentinel-data-sentinel/** tb-node/redis-data/** !.env diff --git a/docker/README.md b/docker/README.md index 71ea87f353..1a852c80eb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -21,8 +21,9 @@ In order to set cache type change the value of `CACHE` variable in `.env` file t - `redis` - use Redis standalone cache (1 node - 1 master); - `redis-cluster` - use Redis cluster cache (6 nodes - 3 masters, 3 slaves); +- `redis-sentinel` - use Redis sentinel cache (3 nodes - 1 master, 1 slave, 1 sentinel) -**NOTE**: According to the cache type corresponding docker service will be deployed (see `docker-compose.redis.yml`, `docker-compose.redis-cluster.yml` for details). +**NOTE**: According to the cache type corresponding docker service will be deployed (see `docker-compose.redis.yml`, `docker-compose.redis-cluster.yml`, `docker-compose.redis-sentinel.yml` for details). Execute the following command to create log folders for the services and chown of these folders to the docker container users. To be able to change user, **chown** command is used, which requires sudo permissions (script will request password for a sudo access): diff --git a/docker/cache-redis-sentinel.env b/docker/cache-redis-sentinel.env new file mode 100644 index 0000000000..39a1246d9c --- /dev/null +++ b/docker/cache-redis-sentinel.env @@ -0,0 +1,7 @@ +CACHE_TYPE=redis +REDIS_CONNECTION_TYPE=sentinel +REDIS_MASTER=mymaster +REDIS_SENTINELS=redis-sentinel:26379 +REDIS_SENTINEL_PASSWORD=sentinel +REDIS_USE_DEFAULT_POOL_CONFIG=false +REDIS_PASSWORD=thingsboard diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index 49f3e20e27..19ca342b9b 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -84,8 +84,11 @@ function additionalComposeCacheArgs() { redis-cluster) CACHE_COMPOSE_ARGS="-f docker-compose.redis-cluster.yml" ;; + redis-sentinel) + CACHE_COMPOSE_ARGS="-f docker-compose.redis-sentinel.yml" + ;; *) - echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster'." >&2 + echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster' or 'redis-sentinel'." >&2 exit 1 esac echo $CACHE_COMPOSE_ARGS @@ -114,8 +117,11 @@ function additionalStartupServices() { redis-cluster) ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5" ;; + redis-sentinel) + ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES redis-master redis-slave redis-sentinel" + ;; *) - echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster'." >&2 + echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster' or 'redis-sentinel'." >&2 exit 1 esac @@ -160,8 +166,15 @@ function permissionList() { 1001 1001 tb-node/redis-cluster-data-5 " ;; + redis-sentinel) + PERMISSION_LIST="$PERMISSION_LIST + 1001 1001 tb-node/redis-sentinel-data-master + 1001 1001 tb-node/redis-sentinel-data-slave + 1001 1001 tb-node/redis-sentinel-data-sentinel + " + ;; *) - echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster'." >&2 + echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster' or 'redis-sentinel'." >&2 exit 1 esac diff --git a/docker/docker-compose.redis-sentinel.volumes.yml b/docker/docker-compose.redis-sentinel.volumes.yml new file mode 100644 index 0000000000..38534368ea --- /dev/null +++ b/docker/docker-compose.redis-sentinel.volumes.yml @@ -0,0 +1,40 @@ +# +# 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. +# + +version: '3.0' + +services: + # Redis sentinel + redis-master: + volumes: + - redis-sentinel-data-master:/bitnami/redis/data + redis-slave: + volumes: + - redis-sentinel-data-slave:/bitnami/redis/data + redis-sentinel: + volumes: + - redis-sentinel-data-sentinel:/bitnami/redis/data + +volumes: + redis-sentinel-data-master: + external: + name: ${REDIS_SENTINEL_DATA_VOLUME_MASTER} + redis-sentinel-data-slave: + external: + name: ${REDIS_SENTINEL_DATA_VOLUME_SLAVE} + redis-sentinel-data-sentinel: + external: + name: ${REDIS_SENTINEL_DATA_VOLUME_SENTINEL} diff --git a/docker/docker-compose.redis-sentinel.yml b/docker/docker-compose.redis-sentinel.yml new file mode 100644 index 0000000000..32b3f1e826 --- /dev/null +++ b/docker/docker-compose.redis-sentinel.yml @@ -0,0 +1,119 @@ +# +# 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. +# + +version: '3.0' + +services: + # Redis sentinel + redis-master: + image: 'bitnami/redis:7.0' + volumes: + - ./tb-node/redis-sentinel-data-master:/bitnami/redis/data + environment: + - 'REDIS_REPLICATION_MODE=master' + - 'REDIS_PASSWORD=thingsboard' + + redis-slave: + image: 'bitnami/redis:7.0' + volumes: + - ./tb-node/redis-sentinel-data-slave:/bitnami/redis/data + environment: + - 'REDIS_REPLICATION_MODE=slave' + - 'REDIS_MASTER_HOST=redis-master' + - 'REDIS_MASTER_PASSWORD=thingsboard' + - 'REDIS_PASSWORD=thingsboard' + depends_on: + - redis-master + + redis-sentinel: + image: 'bitnami/redis-sentinel:7.0' + volumes: + - ./tb-node/redis-sentinel-data-sentinel:/bitnami/redis/data + environment: + - 'REDIS_MASTER_HOST=redis-master' + - 'REDIS_MASTER_SET=mymaster' + - 'REDIS_SENTINEL_PASSWORD=sentinel' + - 'REDIS_MASTER_PASSWORD=thingsboard' + depends_on: + - redis-master + - redis-slave + + # ThingsBoard setup to use redis-sentinel + tb-core1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-core2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-rule-engine1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-rule-engine2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-mqtt-transport1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-mqtt-transport2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-http-transport1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-http-transport2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-coap-transport: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-lwm2m-transport: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-snmp-transport: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-vc-executor1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-vc-executor2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index 2daf5c5b7b..399d0f4a56 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -16,21 +16,47 @@ package org.thingsboard.monitoring; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.EnableScheduling; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.monitoring.service.BaseMonitoringService; +import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; @SpringBootApplication @EnableScheduling @Slf4j public class ThingsboardMonitoringApplication { + @Autowired + private List> monitoringServices; + + @Value("${monitoring.monitoring_rate_ms}") + private int monitoringRateMs; + public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) .properties(Map.of("spring.config.name", "tb-monitoring")) .run(args); } + @EventListener(ApplicationReadyEvent.class) + public void startMonitoring() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor")); + scheduler.scheduleWithFixedDelay(() -> { + monitoringServices.forEach(monitoringService -> { + monitoringService.runChecks(); + }); + }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java similarity index 84% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java index 5f1ab49e91..4304ecdf0e 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java @@ -15,12 +15,10 @@ */ package org.thingsboard.monitoring.config; -import lombok.Data; +import java.util.List; -@Data -public class MonitoringTargetConfig { +public interface MonitoringConfig { - private String baseUrl; - private DeviceConfig device; + List getTargets(); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java new file mode 100644 index 0000000000..0e62670f81 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java @@ -0,0 +1,24 @@ +/** + * 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. + */ +package org.thingsboard.monitoring.config; + +import java.util.UUID; + +public interface MonitoringTarget { + + UUID getDeviceId(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java similarity index 91% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java index 905858b4eb..a95aca18f4 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.coap.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java similarity index 92% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java index 548ad6d08b..94c0ea3a84 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config; +package org.thingsboard.monitoring.config.transport; import lombok.Data; import org.apache.commons.lang3.StringUtils; @@ -25,6 +25,7 @@ import java.util.UUID; public class DeviceConfig { private UUID id; + private String name; private DeviceCredentials credentials; public void setId(String id) { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java similarity index 91% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java index 3a3e8f612c..3b2be2343d 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.http.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java similarity index 91% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java index 4d97de8366..7d375abd6e 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.lwm2m.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java similarity index 92% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java index 7fef5b95dc..ebc91a327e 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.mqtt.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java similarity index 80% rename from monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java index d7619ea32d..6fb2e25ec9 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.data; +package org.thingsboard.monitoring.config.transport; import lombok.Data; -import org.thingsboard.monitoring.config.TransportType; @Data public class TransportInfo { private final TransportType transportType; - private final String url; + private final String baseUrl; @Override public String toString() { - return String.format("%s (%s)", transportType, url); + return String.format("%s transport (%s)", transportType, baseUrl); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java similarity index 73% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java index 0712d1d919..77d702f779 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import lombok.Data; -import org.thingsboard.monitoring.config.MonitoringTargetConfig; -import org.thingsboard.monitoring.config.TransportType; +import org.thingsboard.monitoring.config.MonitoringConfig; import java.util.List; @Data -public abstract class TransportMonitoringConfig { +public abstract class TransportMonitoringConfig implements MonitoringConfig { private int requestTimeoutMs; - private List targets; + private List targets; public abstract TransportType getTransportType(); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java new file mode 100644 index 0000000000..816f64fbce --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java @@ -0,0 +1,34 @@ +/** + * 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. + */ +package org.thingsboard.monitoring.config.transport; + +import lombok.Data; +import org.thingsboard.monitoring.config.MonitoringTarget; + +import java.util.UUID; + +@Data +public class TransportMonitoringTarget implements MonitoringTarget { + + private String baseUrl; + private DeviceConfig device; // set manually during initialization + + @Override + public UUID getDeviceId() { + return device.getId(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java similarity index 67% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java index a3aaa7ec98..eeb085348b 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config; +package org.thingsboard.monitoring.config.transport; import lombok.AllArgsConstructor; import lombok.Getter; -import org.thingsboard.monitoring.transport.TransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.CoapTransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.HttpTransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.Lwm2mTransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.MqttTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.CoapTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.HttpTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.Lwm2mTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.MqttTransportHealthChecker; @AllArgsConstructor @Getter diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java index 8141c34586..3370d42462 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -15,16 +15,14 @@ */ package org.thingsboard.monitoring.data; -import org.thingsboard.monitoring.config.TransportType; - public class Latencies { public static final String WS_UPDATE = "wsUpdate"; public static final String WS_CONNECT = "wsConnect"; public static final String LOG_IN = "logIn"; - public static String transportRequest(TransportType transportType) { - return String.format("%sTransportRequest", transportType.name().toLowerCase()); + public static String request(String key) { + return String.format("%sRequest", key); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java similarity index 80% rename from monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java rename to monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java index 8157c4af5f..dc91a56a3c 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java @@ -15,13 +15,13 @@ */ package org.thingsboard.monitoring.data; -public class TransportFailureException extends RuntimeException { +public class ServiceFailureException extends RuntimeException { - public TransportFailureException(Throwable cause) { + public ServiceFailureException(Throwable cause) { super(cause.getMessage(), cause); } - public TransportFailureException(String message) { + public ServiceFailureException(String message) { super(message); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java similarity index 54% rename from monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java rename to monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index 0c742e7709..affca1ce09 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -13,34 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.transport; +package org.thingsboard.monitoring.service; -import com.fasterxml.jackson.databind.node.TextNode; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.TbClient; import org.thingsboard.monitoring.client.WsClient; -import org.thingsboard.monitoring.config.MonitoringTargetConfig; -import org.thingsboard.monitoring.config.TransportType; -import org.thingsboard.monitoring.config.service.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.MonitoringConfig; +import org.thingsboard.monitoring.config.MonitoringTarget; import org.thingsboard.monitoring.data.Latencies; import org.thingsboard.monitoring.data.MonitoredServiceKey; -import org.thingsboard.monitoring.data.TransportFailureException; -import org.thingsboard.monitoring.data.TransportInfo; -import org.thingsboard.monitoring.service.MonitoringReporter; +import org.thingsboard.monitoring.data.ServiceFailureException; import org.thingsboard.monitoring.util.TbStopWatch; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.UUID; +@RequiredArgsConstructor @Slf4j -public abstract class TransportHealthChecker { +public abstract class BaseHealthChecker { protected final C config; - protected final MonitoringTargetConfig target; - private TransportInfo transportInfo; + protected final T target; + + private Object info; @Autowired private MonitoringReporter reporter; @@ -51,73 +50,66 @@ public abstract class TransportHealthChecker, T extends MonitoringTarget> { + + @Autowired + private List configs; + private final List> healthCheckers = new LinkedList<>(); + private final List devices = new LinkedList<>(); + + @Autowired + private TbClient tbClient; + @Autowired + private WsClientFactory wsClientFactory; + @Autowired + private TbStopWatch stopWatch; + @Autowired + private MonitoringReporter reporter; + @Autowired + protected ApplicationContext applicationContext; + + @PostConstruct + private void init() { + tbClient.logIn(); + configs.forEach(config -> { + config.getTargets().forEach(target -> { + BaseHealthChecker healthChecker = (BaseHealthChecker) createHealthChecker(config, target); + log.info("Initializing {}", healthChecker.getClass().getSimpleName()); + healthChecker.initialize(tbClient); + devices.add(target.getDeviceId()); + healthCheckers.add(healthChecker); + }); + }); + } + + public final void runChecks() { + if (healthCheckers.isEmpty()) { + return; + } + try { + log.info("Starting {}", getName()); + stopWatch.start(); + String accessToken = tbClient.logIn(); + reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); + + try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { + wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); + + for (BaseHealthChecker healthChecker : healthCheckers) { + healthChecker.check(wsClient); + } + } + reporter.reportLatencies(tbClient); + log.debug("Finished {}", getName()); + } catch (Throwable error) { + try { + reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); + } catch (Throwable reportError) { + log.error("Error occurred during service failure reporting", reportError); + } + } + } + + protected abstract BaseHealthChecker createHealthChecker(C config, T target); + + protected abstract String getName(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java index 4649e31ac4..f41454a76a 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -52,8 +52,8 @@ public class MonitoringReporter { @Value("${monitoring.failures_threshold}") private int failuresThreshold; - @Value("${monitoring.send_repeated_failure_notification}") - private boolean sendRepeatedFailureNotification; + @Value("${monitoring.repeated_failure_notification}") + private int repeatedFailureNotification; @Value("${monitoring.latency.enabled}") private boolean latencyReportingEnabled; @@ -75,7 +75,7 @@ public class MonitoringReporter { return; } log.info("Latencies:\n{}", latencies.stream().map(latency -> latency.getKey() + ": " + latency.getAvg() + " ms") - .collect(Collectors.joining("\n"))); + .collect(Collectors.joining("\n")) + "\n"); if (!latencyReportingEnabled) return; @@ -86,7 +86,7 @@ public class MonitoringReporter { try { if (StringUtils.isBlank(reportingAssetId)) { - String assetName = "Monitoring"; + String assetName = "[Monitoring] Latencies"; Asset monitoringAsset = tbClient.findAsset(assetName).orElseGet(() -> { Asset asset = new Asset(); asset.setType("Monitoring"); @@ -122,7 +122,7 @@ public class MonitoringReporter { int failuresCount = failuresCounters.computeIfAbsent(serviceKey, k -> new AtomicInteger()).incrementAndGet(); ServiceFailureNotification notification = new ServiceFailureNotification(serviceKey, error, failuresCount); log.error(notification.getText()); - if (failuresCount == failuresThreshold || (sendRepeatedFailureNotification && failuresCount % failuresThreshold == 0)) { + if (failuresCount == failuresThreshold || (repeatedFailureNotification != 0 && failuresCount % repeatedFailureNotification == 0)) { notificationService.sendNotification(notification); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java new file mode 100644 index 0000000000..c822720f23 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -0,0 +1,139 @@ +/** + * 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. + */ +package org.thingsboard.monitoring.service.transport; + +import com.fasterxml.jackson.databind.node.TextNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.config.transport.DeviceConfig; +import org.thingsboard.monitoring.config.transport.TransportInfo; +import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.BaseHealthChecker; +import org.thingsboard.monitoring.util.ResourceUtils; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; + +@Slf4j +public abstract class TransportHealthChecker extends BaseHealthChecker { + + private static final String DEFAULT_DEVICE_NAME = "[Monitoring] %s transport (%s)"; + private static final String DEFAULT_PROFILE_NAME = "[Monitoring] %s"; + + public TransportHealthChecker(C config, TransportMonitoringTarget target) { + super(config, target); + } + + @Override + protected void initialize(TbClient tbClient) { + String deviceName = String.format(DEFAULT_DEVICE_NAME, config.getTransportType(), target.getBaseUrl()); + Device device = tbClient.getTenantDevice(deviceName) + .orElseGet(() -> { + log.info("Creating new device '{}'", deviceName); + return createDevice(config.getTransportType(), deviceName, tbClient); + }); + DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(device.getId()) + .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + device.getId())); + + DeviceConfig deviceConfig = new DeviceConfig(); + deviceConfig.setId(device.getId().toString()); + deviceConfig.setName(deviceName); + deviceConfig.setCredentials(credentials); + target.setDevice(deviceConfig); + } + + @Override + protected String createTestPayload(String testValue) { + return JacksonUtil.newObjectNode().set(TEST_TELEMETRY_KEY, new TextNode(testValue)).toString(); + } + + @Override + protected Object getInfo() { + return new TransportInfo(getTransportType(), target.getBaseUrl()); + } + + @Override + protected String getKey() { + return getTransportType().name().toLowerCase() + "Transport"; + } + + protected abstract TransportType getTransportType(); + + + private Device createDevice(TransportType transportType, String name, TbClient tbClient) { + Device device = new Device(); + device.setName(name); + + DeviceCredentials credentials = new DeviceCredentials(); + credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); + + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + if (transportType != TransportType.LWM2M) { + device.setType("default"); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + } else { + tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() + .stream().findFirst() + .orElseGet(() -> { + TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); + log.info("Creating LwM2M resource"); + return tbClient.saveResource(newResource); + }); + String profileName = String.format(DEFAULT_PROFILE_NAME, transportType); + DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() + .stream().findFirst() + .orElseGet(() -> { + DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); + newProfile.setName(profileName); + log.info("Creating LwM2M device profile"); + return tbClient.saveDeviceProfile(newProfile); + }); + device.setType(profileName); + device.setDeviceProfileId(profile.getId()); + deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); + + credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); + NoSecClientCredential client = new NoSecClientCredential(); + client.setEndpoint(credentials.getCredentialsId()); + lwm2mCreds.setClient(client); + LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); + bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); + bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); + lwm2mCreds.setBootstrap(bootstrap); + credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); + } + return tbClient.saveDeviceWithCredentials(device, credentials).get(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java new file mode 100644 index 0000000000..b3ce86e799 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java @@ -0,0 +1,41 @@ +/** + * 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. + */ +package org.thingsboard.monitoring.service.transport; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.service.BaseHealthChecker; +import org.thingsboard.monitoring.service.BaseMonitoringService; + +@Service +@RequiredArgsConstructor +@Slf4j +public final class TransportsMonitoringService extends BaseMonitoringService { + + @Override + protected BaseHealthChecker createHealthChecker(TransportMonitoringConfig config, TransportMonitoringTarget target) { + return applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); + } + + @Override + protected String getName() { + return "transports check"; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java similarity index 86% rename from monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java rename to monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java index 3e36e62c58..f56415fa6a 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.transport.impl; +package org.thingsboard.monitoring.service.transport.impl; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; @@ -23,10 +23,10 @@ import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.MonitoringTargetConfig; -import org.thingsboard.monitoring.config.TransportType; -import org.thingsboard.monitoring.config.service.CoapTransportMonitoringConfig; -import org.thingsboard.monitoring.transport.TransportHealthChecker; +import org.thingsboard.monitoring.config.transport.CoapTransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; import java.io.IOException; @@ -37,7 +37,7 @@ public class CoapTransportHealthChecker extends TransportHealthChecker configs; - private final List> transportHealthCheckers = new LinkedList<>(); - private final List devices = new LinkedList<>(); - - private final TbClient tbClient; - private final WsClientFactory wsClientFactory; - private final TbStopWatch stopWatch; - private final MonitoringReporter reporter; - private final ApplicationContext applicationContext; - private ScheduledExecutorService scheduler; - @Value("${monitoring.transports.monitoring_rate_ms}") - private int monitoringRateMs; - - @PostConstruct - private void init() { - configs.forEach(config -> { - config.getTargets().stream() - .filter(target -> StringUtils.isNotBlank(target.getBaseUrl())) - .peek(target -> checkMonitoringTarget(config, target, tbClient)) - .forEach(target -> { - TransportHealthChecker transportHealthChecker = applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); - transportHealthCheckers.add(transportHealthChecker); - devices.add(target.getDevice().getId()); - }); - }); - scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor")); - } - - @EventListener(ApplicationReadyEvent.class) - public void startMonitoring() { - scheduler.scheduleWithFixedDelay(() -> { - try { - log.debug("Starting transports check"); - stopWatch.start(); - String accessToken = tbClient.logIn(); - reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); - - try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { - wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); - - for (TransportHealthChecker transportHealthChecker : transportHealthCheckers) { - transportHealthChecker.check(wsClient); - } - } - reporter.reportLatencies(tbClient); - log.debug("Finished transports check"); - } catch (Throwable error) { - try { - reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); - } catch (Throwable reportError) { - log.error("Error occurred during service failure reporting", reportError); - } - } - }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); - } - - private void checkMonitoringTarget(TransportMonitoringConfig config, MonitoringTargetConfig target, TbClient tbClient) { - DeviceConfig deviceConfig = target.getDevice(); - tbClient.logIn(); - - DeviceId deviceId; - if (deviceConfig == null || deviceConfig.getId() == null) { - String deviceName = String.format("[%s] Monitoring device (%s)", config.getTransportType(), target.getBaseUrl()); - Device device = tbClient.getTenantDevice(deviceName) - .orElseGet(() -> { - log.info("Creating new device '{}'", deviceName); - return createDevice(config.getTransportType(), deviceName, tbClient); - }); - deviceId = device.getId(); - target.getDevice().setId(deviceId.toString()); - } else { - deviceId = new DeviceId(deviceConfig.getId()); - } - - log.info("Using device {} for {} monitoring", deviceId, config.getTransportType()); - DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(deviceId) - .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + deviceId)); - target.getDevice().setCredentials(credentials); - } - - private Device createDevice(TransportType transportType, String name, TbClient tbClient) { - Device device = new Device(); - device.setName(name); - - DeviceCredentials credentials = new DeviceCredentials(); - credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); - - DeviceData deviceData = new DeviceData(); - deviceData.setConfiguration(new DefaultDeviceConfiguration()); - if (transportType != TransportType.LWM2M) { - device.setType("default"); - deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); - credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); - } else { - tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() - .stream().findFirst() - .orElseGet(() -> { - TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); - log.info("Creating LwM2M resource"); - return tbClient.saveResource(newResource); - }); - String profileName = "LwM2M Monitoring"; - DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() - .stream().findFirst() - .orElseGet(() -> { - DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); - newProfile.setName(profileName); - log.info("Creating LwM2M device profile"); - return tbClient.saveDeviceProfile(newProfile); - }); - device.setType(profileName); - device.setDeviceProfileId(profile.getId()); - deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); - - credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); - LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); - NoSecClientCredential client = new NoSecClientCredential(); - client.setEndpoint(credentials.getCredentialsId()); - lwm2mCreds.setClient(client); - LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); - bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); - bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); - lwm2mCreds.setBootstrap(bootstrap); - credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); - } - return tbClient.saveDeviceWithCredentials(device, credentials).get(); - } - -} diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json index 7f93a7e6b6..095f4859e9 100644 --- a/monitoring/src/main/resources/lwm2m/device_profile.json +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -1,5 +1,4 @@ { - "name": "LwM2M Monitoring", "type": "DEFAULT", "image": null, "defaultQueueName": null, diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml index d89886cd72..88979bff1a 100644 --- a/monitoring/src/main/resources/tb-monitoring.yml +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -32,79 +32,62 @@ monitoring: # WebSocket request timeout request_timeout_ms: '${WS_REQUEST_TIMEOUT_MS:3000}' + # Checks frequency in milliseconds + monitoring_rate_ms: '${MONITORING_RATE_MS:10000}' # Maximum time between request to transport and WebSocket update check_timeout_ms: '${CHECK_TIMEOUT_MS:5000}' # Failures threshold for notifying failures_threshold: '${FAILURES_THRESHOLD:2}' - # Whether to notify about next failures after first notification (will notify after each FAILURES_THRESHOLD failures) - send_repeated_failure_notification: '${SEND_REPEATED_FAILURE_NOTIFICATION:true}' + # Notify after each REPEATED_FAILURE_NOTIFICATION subsequent failures, 0 to notify only once on first failure + repeated_failure_notification: '${REPEATED_FAILURE_NOTIFICATION:4}' transports: - # Transports check frequency in milliseconds - monitoring_rate_ms: '${TRANSPORTS_MONITORING_RATE_MS:10000}' - mqtt: - # Enable MQTT checks + # Enable MQTT transport checks enabled: '${MQTT_TRANSPORT_MONITORING_ENABLED:true}' # MQTT request timeout in milliseconds request_timeout_ms: '${MQTT_REQUEST_TIMEOUT_MS:4000}' # MQTT QoS qos: '${MQTT_QOS_LEVEL:1}' targets: - # MQTT base url, tcp://DOMAIN:1883 by default + # MQTT transport base url, tcp://DOMAIN:1883 by default - base_url: '${MQTT_TRANSPORT_BASE_URL:tcp://${monitoring.domain}:1883}' - device: - # MQTT device to push telemetry for. If not set - device will be found or created automatically - id: '${MQTT_TRANSPORT_TARGET_DEVICE_ID:}' # To add more targets, use following environment variables: - # monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[1].device.id, - # monitoring.transports.mqtt.targets[2].base_url, monitoring.transports.mqtt.targets[2].device.id, etc. + # monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[2].base_url, etc. coap: - # Enable CoAP checks + # Enable CoAP transport checks enabled: '${COAP_TRANSPORT_MONITORING_ENABLED:true}' # CoAP request timeout in milliseconds request_timeout_ms: '${COAP_REQUEST_TIMEOUT_MS:4000}' targets: - # CoAP base url, coap://DOMAIN by default + # CoAP transport base url, coap://DOMAIN by default - base_url: '${COAP_TRANSPORT_BASE_URL:coap://${monitoring.domain}}' - # CoAP device to push telemetry for. If not set - device will be found or created automatically - device: - id: '${COAP_TRANSPORT_TARGET_DEVICE_ID:}' # To add more targets, use following environment variables: - # monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[1].device.id, - # monitoring.transports.coap.targets[2].base_url, monitoring.transports.coap.targets[2].device.id, etc. + # monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[2].base_url, etc. http: - # Enable HTTP checks + # Enable HTTP transport checks enabled: '${HTTP_TRANSPORT_MONITORING_ENABLED:true}' # HTTP request timeout in milliseconds request_timeout_ms: '${HTTP_REQUEST_TIMEOUT_MS:4000}' targets: - # HTTP base url, https://DOMAIN by default - - base_url: '${HTTP_TRANSPORT_BASE_URL:https://${monitoring.domain}}' - device: - # HTTP device to push telemetry for. If not set - device will be found or created automatically - id: '${HTTP_TRANSPORT_TARGET_DEVICE_ID:}' + # HTTP transport base url, http://DOMAIN by default + - base_url: '${HTTP_TRANSPORT_BASE_URL:http://${monitoring.domain}}' # To add more targets, use following environment variables: - # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[1].device.id, - # monitoring.transports.http.targets[2].base_url, monitoring.transports.http.targets[2].device.id, etc. + # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[2].base_url, etc. lwm2m: - # Enable LwM2M checks + # Enable LwM2M transport checks enabled: '${LWM2M_TRANSPORT_MONITORING_ENABLED:true}' # LwM2M request timeout in milliseconds request_timeout_ms: '${LWM2M_REQUEST_TIMEOUT_MS:4000}' targets: - # LwM2M base url, coap://DOMAIN:5685 by default + # LwM2M transport base url, coap://DOMAIN:5685 by default - base_url: '${LWM2M_TRANSPORT_BASE_URL:coap://${monitoring.domain}:5685}' - # LwM2M device to push telemetry for. If not set - device will be found or created automatically - device: - id: '${LWM2M_TRANSPORT_TARGET_DEVICE_ID:}' # To add more targets, use following environment variables: - # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[1].device.id, - # monitoring.transports.lwm2m.targets[2].base_url, monitoring.transports.lwm2m.targets[2].device.id, etc. + # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[2].base_url, etc. notification_channels: slack: diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index 4a88d5e8cd..340f0d0eb8 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -26,6 +26,10 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.redisCluster=true +- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis sentinel: + + mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.redisSentinel=true + - Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory in Hybrid mode (postgres + cassandra): mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.hybridMode=true diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index ffbe14ed52..dd3948c495 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -43,6 +43,7 @@ import static org.testng.Assert.fail; @Slf4j public class ContainerTestSuite { final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); + final static boolean IS_REDIS_SENTINEL = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisSentinel")); final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode")); final static String QUEUE_TYPE = System.getProperty("blackBoxTests.queue", "kafka"); private static final String SOURCE_DIR = "./../../docker/"; @@ -80,8 +81,9 @@ public class ContainerTestSuite { installTb = new ThingsBoardDbInstaller(); installTb.createVolumes(); log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER); + log.info("System property of blackBoxTests.redisSentinel is {}", IS_REDIS_SENTINEL); log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE); - boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); + boolean skipTailChildContainers = Boolean.parseBoolean(System.getProperty("blackBoxTests.skipTailChildContainers")); try { final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/"; log.info("targetDir {}", targetDir); @@ -109,8 +111,8 @@ public class ContainerTestSuite { new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid-test-extras.yml" : "docker-compose.postgres-test-extras.yml")), new File(targetDir + "docker-compose.postgres.volumes.yml"), new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"), - new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")), - new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml")), + new File(targetDir + resolveRedisComposeFile()), + new File(targetDir + resolveRedisComposeVolumesFile()), new File(targetDir + ("docker-selenium.yml")) )); @@ -154,6 +156,7 @@ public class ContainerTestSuite { testContainer = new DockerComposeContainerImpl<>(composeFiles) .withPull(false) .withLocalCompose(true) + .withOptions("--compatibility") .withTailChildContainers(!skipTailChildContainers) .withEnv(installTb.getEnv()) .withEnv(queueEnv) @@ -175,6 +178,27 @@ public class ContainerTestSuite { fail("Failed to create test container"); } } + + private static String resolveRedisComposeFile() { + if (IS_REDIS_CLUSTER) { + return "docker-compose.redis-cluster.yml"; + } + if (IS_REDIS_SENTINEL) { + return "docker-compose.redis-sentinel.yml"; + } + return "docker-compose.redis.yml"; + } + + private static String resolveRedisComposeVolumesFile() { + if (IS_REDIS_CLUSTER) { + return "docker-compose.redis-cluster.volumes.yml"; + } + if (IS_REDIS_SENTINEL) { + return "docker-compose.redis-sentinel.volumes.yml"; + } + return "docker-compose.redis.volumes.yml"; + } + public void stop() { if (isActive) { testContainer.stop(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index e6c9856094..e41bbc3449 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -32,12 +32,14 @@ import java.util.stream.IntStream; public class ThingsBoardDbInstaller { final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); + final static boolean IS_REDIS_SENTINEL = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisSentinel")); final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode")); private final static String POSTGRES_DATA_VOLUME = "tb-postgres-test-data-volume"; private final static String CASSANDRA_DATA_VOLUME = "tb-cassandra-test-data-volume"; private final static String REDIS_DATA_VOLUME = "tb-redis-data-volume"; private final static String REDIS_CLUSTER_DATA_VOLUME = "tb-redis-cluster-data-volume"; + private final static String REDIS_SENTINEL_DATA_VOLUME = "tb-redis-sentinel-data-volume"; private final static String TB_LOG_VOLUME = "tb-log-test-volume"; private final static String TB_COAP_TRANSPORT_LOG_VOLUME = "tb-coap-transport-log-test-volume"; private final static String TB_LWM2M_TRANSPORT_LOG_VOLUME = "tb-lwm2m-transport-log-test-volume"; @@ -54,6 +56,7 @@ public class ThingsBoardDbInstaller { private final String redisDataVolume; private final String redisClusterDataVolume; + private final String redisSentinelDataVolume; private final String tbLogVolume; private final String tbCoapTransportLogVolume; private final String tbLwm2mTransportLogVolume; @@ -65,6 +68,7 @@ public class ThingsBoardDbInstaller { public ThingsBoardDbInstaller() { log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER); + log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_SENTINEL); log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE); List composeFiles = new ArrayList<>(Arrays.asList( new File("./../../docker/docker-compose.yml"), @@ -73,12 +77,8 @@ public class ThingsBoardDbInstaller { ? new File("./../../docker/docker-compose.hybrid.yml") : new File("./../../docker/docker-compose.postgres.yml"), new File("./../../docker/docker-compose.postgres.volumes.yml"), - IS_REDIS_CLUSTER - ? new File("./../../docker/docker-compose.redis-cluster.yml") - : new File("./../../docker/docker-compose.redis.yml"), - IS_REDIS_CLUSTER - ? new File("./../../docker/docker-compose.redis-cluster.volumes.yml") - : new File("./../../docker/docker-compose.redis.volumes.yml") + resolveRedisComposeFile(), + resolveRedisComposeVolumesFile() )); if (IS_HYBRID_MODE) { composeFiles.add(new File("./../../docker/docker-compose.cassandra.volumes.yml")); @@ -94,6 +94,7 @@ public class ThingsBoardDbInstaller { cassandraDataVolume = project + "_" + CASSANDRA_DATA_VOLUME; redisDataVolume = project + "_" + REDIS_DATA_VOLUME; redisClusterDataVolume = project + "_" + REDIS_CLUSTER_DATA_VOLUME; + redisSentinelDataVolume = project + "_" + REDIS_SENTINEL_DATA_VOLUME; tbLogVolume = project + "_" + TB_LOG_VOLUME; tbCoapTransportLogVolume = project + "_" + TB_COAP_TRANSPORT_LOG_VOLUME; tbLwm2mTransportLogVolume = project + "_" + TB_LWM2M_TRANSPORT_LOG_VOLUME; @@ -121,12 +122,36 @@ public class ThingsBoardDbInstaller { for (int i = 0; i < 6; i++) { env.put("REDIS_CLUSTER_DATA_VOLUME_" + i, redisClusterDataVolume + '-' + i); } + } else if (IS_REDIS_SENTINEL) { + env.put("REDIS_SENTINEL_DATA_VOLUME_MASTER", redisSentinelDataVolume + "-" + "master"); + env.put("REDIS_SENTINEL_DATA_VOLUME_SLAVE", redisSentinelDataVolume + "-" + "slave"); + env.put("REDIS_SENTINEL_DATA_VOLUME_SENTINEL", redisSentinelDataVolume + "-" + "sentinel"); } else { env.put("REDIS_DATA_VOLUME", redisDataVolume); } dockerCompose.withEnv(env); } + private static File resolveRedisComposeVolumesFile() { + if (IS_REDIS_CLUSTER) { + return new File("./../../docker/docker-compose.redis-cluster.volumes.yml"); + } + if (IS_REDIS_SENTINEL) { + return new File("./../../docker/docker-compose.redis-sentinel.volumes.yml"); + } + return new File("./../../docker/docker-compose.redis.volumes.yml"); + } + + private static File resolveRedisComposeFile() { + if (IS_REDIS_CLUSTER) { + return new File("./../../docker/docker-compose.redis-cluster.yml"); + } + if (IS_REDIS_SENTINEL) { + return new File("./../../docker/docker-compose.redis-sentinel.yml"); + } + return new File("./../../docker/docker-compose.redis.yml"); + } + public Map getEnv() { return env; } @@ -163,18 +188,30 @@ public class ThingsBoardDbInstaller { dockerCompose.withCommand("volume create " + tbVcExecutorLogVolume); dockerCompose.invokeDocker(); - String additionalServices = ""; + StringBuilder additionalServices = new StringBuilder(); if (IS_HYBRID_MODE) { - additionalServices += " cassandra"; + additionalServices.append(" cassandra"); } if (IS_REDIS_CLUSTER) { for (int i = 0; i < 6; i++) { - additionalServices = additionalServices + " redis-node-" + i; + additionalServices.append(" redis-node-").append(i); dockerCompose.withCommand("volume create " + redisClusterDataVolume + '-' + i); dockerCompose.invokeDocker(); } + } else if (IS_REDIS_SENTINEL) { + additionalServices.append(" redis-master"); + dockerCompose.withCommand("volume create " + redisSentinelDataVolume +"-" + "master"); + dockerCompose.invokeDocker(); + + additionalServices.append(" redis-slave"); + dockerCompose.withCommand("volume create " + redisSentinelDataVolume + '-' + "slave"); + dockerCompose.invokeDocker(); + + additionalServices.append(" redis-sentinel"); + dockerCompose.withCommand("volume create " + redisSentinelDataVolume + '-' + "sentinel"); + dockerCompose.invokeDocker(); } else { - additionalServices += " redis"; + additionalServices.append(" redis"); dockerCompose.withCommand("volume create " + redisDataVolume); dockerCompose.invokeDocker(); } @@ -189,7 +226,7 @@ public class ThingsBoardDbInstaller { try { dockerCompose.withCommand("down -v"); dockerCompose.invokeCompose(); - } catch (Exception e) {} + } catch (Exception ignored) {} } } @@ -204,13 +241,22 @@ public class ThingsBoardDbInstaller { dockerCompose.withCommand("volume rm -f " + postgresDataVolume + " " + tbLogVolume + " " + tbCoapTransportLogVolume + " " + tbLwm2mTransportLogVolume + " " + tbHttpTransportLogVolume + - " " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume + - (IS_REDIS_CLUSTER - ? IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + '-' + i).collect(Collectors.joining()) - : redisDataVolume)); + " " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume + resolveRedisComposeVolumeLog()); dockerCompose.invokeDocker(); } + private String resolveRedisComposeVolumeLog() { + if (IS_REDIS_CLUSTER) { + return IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + "-" + i).collect(Collectors.joining()); + } + if (IS_REDIS_SENTINEL) { + return redisSentinelDataVolume + "-" + "master " + " " + + redisSentinelDataVolume + "-" + "slave" + " " + + redisSentinelDataVolume + " " + "sentinel"; + } + return redisDataVolume; + } + private void copyLogs(String volumeName, String targetDir) { File tbLogsDir = new File(targetDir); tbLogsDir.mkdirs(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java index cfb282b88c..57852fa58e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java @@ -18,6 +18,7 @@ package org.thingsboard.server.msa.ui.base; import lombok.SneakyThrows; import org.openqa.selenium.By; import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; @@ -42,7 +43,6 @@ abstract public class AbstractBasePage { protected Actions actions; protected JavascriptExecutor js; - public AbstractBasePage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofMillis(WAIT_TIMEOUT)); @@ -154,7 +154,11 @@ abstract public class AbstractBasePage { } public void waitUntilAttributeContains(WebElement element, String attribute, String value) { - wait.until(ExpectedConditions.attributeContains(element, attribute, value)); + try { + wait.until(ExpectedConditions.attributeContains(element, attribute, value)); + } catch (WebDriverException e) { + fail("Failed to wait until attribute '" + attribute + "' of element '" + element + "' contains value '" + value + "'"); + } } public void goToNextTab(int tabNumber) { @@ -189,4 +193,25 @@ abstract public class AbstractBasePage { public void pull(WebElement element, int xOffset, int yOffset) { actions.clickAndHold(element).moveByOffset(xOffset, yOffset).release().perform(); } + + public void waitUntilAttributeToBe(String locator, String attribute, String value) { + try { + wait.until(ExpectedConditions.attributeToBe(By.xpath(locator), attribute, value)); + } catch (WebDriverException e) { + fail("Failed to wait until attribute '" + attribute + "' of element located by '" + locator + "' is '" + value + "'"); + } + } + + public void clearInputField(WebElement element) { + element.click(); + element.sendKeys(Keys.CONTROL + "A" + Keys.BACK_SPACE); + } + + public void waitUntilAttributeToBeNotEmpty(WebElement element, String attribute) { + try { + wait.until(ExpectedConditions.attributeToBeNotEmpty(element, attribute)); + } catch (WebDriverException e) { + fail("Failed to wait until attribute '" + attribute + "' of element '" + element + "' is not empty"); + } + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java index 4de6fbaf17..df3bc557bc 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java @@ -157,6 +157,13 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { .findFirst().orElse(null); } + public List getDevicesByName(List deviceNames) { + List allDevices = testRestClient.getDevices(pageLink).getData(); + return allDevices.stream() + .filter(device -> deviceNames.contains(device.getName())) + .collect(Collectors.toList()); + } + public List getRuleChainsByName(String name) { return testRestClient.getRuleChains(pageLink).getData().stream() .filter(s -> s.getName().equals(name)) @@ -174,13 +181,10 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { } public DeviceProfile getDeviceProfileByName(String name) { - try { - return testRestClient.getDeviceProfiles(pageLink).getData().stream() - .filter(x -> x.getName().equals(name)).collect(Collectors.toList()).get(0); - } catch (Exception e) { - log.error("No such device profile with name: " + name); - return null; - } + return testRestClient.getDeviceProfiles(pageLink).getData().stream() + .filter(x -> x.getName().equals(name)) + .findFirst() + .orElse(null); } public AssetProfile getAssetProfileByName(String name) { @@ -226,13 +230,9 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { } } - public WebStorage getWebStorage() { - return webStorage = (WebStorage) driver; - } - public void clearStorage() { - getWebStorage().getLocalStorage().clear(); - getWebStorage().getSessionStorage().clear(); + getJs().executeScript("window.localStorage.clear();"); + getJs().executeScript("window.sessionStorage.clear();"); } public void deleteAlarmById(AlarmId alarmId) { @@ -291,6 +291,15 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { } } + public void deleteDevicesByName(List deviceNames) { + List devices = getDevicesByName(deviceNames); + for (Device device : devices) { + if (device != null) { + testRestClient.deleteDevice(device.getId()); + } + } + } + public void deleteDeviceProfileByTitle(String deviceProfileTitle) { DeviceProfile deviceProfile = getDeviceProfileByName(deviceProfileTitle); if (deviceProfile != null) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmDetailsEntityTabElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmDetailsEntityTabElements.java index 65a8aeb1f1..a75df8c203 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmDetailsEntityTabElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmDetailsEntityTabElements.java @@ -35,6 +35,7 @@ public class AlarmDetailsEntityTabElements extends OtherPageElements { private static final String ALARM_DETAILS_BTN = "//span[text() = '%s']/ancestor::mat-row//mat-icon[contains(text(),'more_horiz')]/parent::button"; private static final String ACCESS_FORBIDDEN_DIALOG_VIEW = "//h2[text() = 'Access Forbidden']/parent::tb-confirm-dialog"; private static final String ALARM_ASSIGNEE_DROPDOWN = "//tb-alarm-assignee-panel"; + private static final String NO_USERS_FOUND_MESSAGE = "//div[@class='tb-not-found-content']/span"; public WebElement assignBtn(String type) { return waitUntilElementToBeClickable(String.format(ASSIGN_BTN, type)); @@ -75,4 +76,8 @@ public class AlarmDetailsEntityTabElements extends OtherPageElements { public WebElement alarmAssigneeDropdown() { return waitUntilVisibilityOfElementLocated(ALARM_ASSIGNEE_DROPDOWN); } + + public WebElement noUsersFoundMessage() { + return waitUntilVisibilityOfElementLocated(NO_USERS_FOUND_MESSAGE); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageHelper.java index ee89e5053d..245864eafa 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageHelper.java @@ -146,6 +146,14 @@ public class CustomerPageHelper extends CustomerPageElements { submitAssignedBtn().click(); } + public void assignedDashboard(String dashboardName) { + plusBtn().click(); + assignedField().click(); + entityFromList(dashboardName).click(); + assignedField().sendKeys(Keys.ESCAPE); + submitAssignedBtn().click(); + } + public boolean customerIsNotPresent(String title) { return elementsIsNotPresent(getEntity(title)); } @@ -169,4 +177,13 @@ public class CustomerPageHelper extends CustomerPageElements { } customerDetailsAlarmsBtn().click(); } + + public void disableHideHomeDashboardToolbar() { + hideHomeDashboardToolbarCheckbox().click(); + waitUntilAttributeToBe("//mat-checkbox[@formcontrolname='homeDashboardHideToolbar']//input", "class", "mdc-checkbox__native-control"); + } + + public void waitUntilDashboardFieldToBeNotEmpty() { + waitUntilAttributeToBeNotEmpty(editMenuDashboardField(), "value"); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java index 8994b2e748..d3ac6db03e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java @@ -30,13 +30,12 @@ public class DashboardPageElements extends OtherPageElementsHelper { private static final String MANAGE_ASSIGNED_ENTITY_LIST_FIELD = "//input[@formcontrolname='entity']"; private static final String MANAGE_ASSIGNED_ENTITY = "//mat-option//span[contains(text(),'%s')]"; private static final String MANAGE_ASSIGNED_UPDATE_BTN = "//button[@type='submit']"; - private static final String EDIT_BTN = "//mat-icon[text() = 'edit']/parent::button"; - private static final String ADD_BTN = "//tb-footer-fab-buttons//button"; - private static final String CREAT_NEW_DASHBOARD_BTN = "//mat-icon[text() = 'insert_drive_file']/parent::button"; + private static final String EDIT_BTN = "//mat-icon[text() = 'edit']/parent::button[@mat-stroked-button]"; + private static final String ADD_BTN = "//mat-fab-actions//mat-icon[text() = 'add']/parent::button"; private static final String ALARM_WIDGET_BUNDLE = "//mat-card-title[text() = 'Alarm widgets']/ancestor::mat-card"; private static final String ALARM_TABLE_WIDGET = "//img[@alt='Alarms table']/ancestor::mat-card"; private static final String WIDGET_SE_CORNER = "//div[contains(@class,'handle-se')]"; - private static final String DONE_BTN = "//tb-footer-fab-buttons/following-sibling::button//mat-icon[text() = 'done']/parent::button"; + private static final String SAVE_BTN = "//mat-icon[text() = 'done']/parent::button[@fxhide.lt-lg]"; public List entityTitles() { return waitUntilVisibilityOfElementsLocated(TITLES); @@ -66,10 +65,6 @@ public class DashboardPageElements extends OtherPageElementsHelper { return waitUntilElementToBeClickable(ADD_BTN); } - public WebElement createNewDashboardBtn() { - return waitUntilElementToBeClickable(CREAT_NEW_DASHBOARD_BTN); - } - public WebElement alarmWidgetBundle() { return waitUntilElementToBeClickable(ALARM_WIDGET_BUNDLE); } @@ -82,7 +77,7 @@ public class DashboardPageElements extends OtherPageElementsHelper { return waitUntilElementToBeClickable(WIDGET_SE_CORNER); } - public WebElement doneBtn() { - return waitUntilVisibilityOfElementLocated(DONE_BTN); + public WebElement saveBtn() { + return waitUntilVisibilityOfElementLocated(SAVE_BTN); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java index 93e8fa10d4..7a89b876f6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java @@ -40,7 +40,6 @@ public class DashboardPageHelper extends DashboardPageElements { public void openSelectWidgetsBundleMenu() { addBtn().click(); - createNewDashboardBtn().click(); } public void openCreateWidgetPopup() { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java index a4404858d9..f6c901c76c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java @@ -18,6 +18,8 @@ package org.thingsboard.server.msa.ui.pages; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import java.util.List; + public class DevicePageElements extends OtherPageElementsHelper { public DevicePageElements(WebDriver driver) { super(driver); @@ -31,7 +33,7 @@ public class DevicePageElements extends OtherPageElementsHelper { private static final String CHOOSE_CUSTOMER_FOR_ASSIGN_FIELD = "//input[@formcontrolname='entity']"; private static final String ENTITY_FROM_DROPDOWN = "//div[@role = 'listbox']//span[text() = '%s']"; private static final String CLOSE_DEVICE_DETAILS_VIEW = "//header//mat-icon[contains(text(),'close')]/parent::button"; - private static final String SUBMIT_ASSIGN_TO_CUSTOMER_BTN = "//button[@type='submit']"; + private static final String SUBMIT_BTN = "//button[@type='submit']"; private static final String ADD_DEVICE_BTN = "//mat-icon[text() = 'insert_drive_file']/parent::button"; private static final String HEADER_NAME_VIEW = "//header//div[@class='tb-details-title']/span"; private static final String ADD_DEVICE_VIEW = "//tb-device-wizard"; @@ -47,12 +49,23 @@ public class DevicePageElements extends OtherPageElementsHelper { private static final String DEVICE_CUSTOMER_PAGE = DEVICE + "/ancestor::mat-row//mat-cell[contains(@class,'cdk-column-customerTitle')]/span"; private static final String DEVICE_LABEL_EDIT = "//input[@formcontrolname='label']"; private static final String DEVICE_DEVICE_PROFILE_PAGE = DEVICE + "/ancestor::mat-row//mat-cell[contains(@class,'cdk-column-deviceProfileName')]/span"; - protected static final String ASSIGN_BTN = ENTITY + "/ancestor::mat-row//mat-icon[contains(text(),'assignment_ind')]/ancestor::button"; - protected static final String UNASSIGN_BTN = ENTITY + "/ancestor::mat-row//mat-icon[contains(text(),' assignment_return')]/ancestor::button"; - protected static final String ASSIGN_BTN_DETAILS_TAB = "//span[contains(text(),'Assign to customer')]/parent::button"; - protected static final String UNASSIGN_BTN_DETAILS_TAB = "//span[contains(text(),'Unassign from customer')]/parent::button"; - protected static final String ASSIGNED_FIELD_DETAILS_TAB = "//mat-label[text() = 'Assigned to customer']/parent::label/parent::div/input"; - protected static final String ASSIGN_MARKED_DEVICE_BTN = "//mat-icon[text() = 'assignment_ind']/parent::button"; + private static final String ASSIGN_BTN = ENTITY + "/ancestor::mat-row//mat-icon[contains(text(),'assignment_ind')]/ancestor::button"; + private static final String UNASSIGN_BTN = ENTITY + "/ancestor::mat-row//mat-icon[contains(text(),' assignment_return')]/ancestor::button"; + private static final String ASSIGN_BTN_DETAILS_TAB = "//span[contains(text(),'Assign to customer')]/parent::button"; + private static final String UNASSIGN_BTN_DETAILS_TAB = "//span[contains(text(),'Unassign from customer')]/parent::button"; + private static final String ASSIGNED_FIELD_DETAILS_TAB = "//mat-label[text() = 'Assigned to customer']/parent::label/parent::div/input"; + private static final String ASSIGN_MARKED_DEVICE_BTN = "//mat-icon[text() = 'assignment_ind']/parent::button"; + private static final String FILTER_BTN = "//tb-device-info-filter/button"; + private static final String DEVICE_PROFILE_FIELD = "(//input[@formcontrolname='deviceProfile'])[2]"; + private static final String DEVICE_STATE_SELECT = "//div[contains(@class,'tb-filter-panel')]//mat-select[@role='combobox']"; + private static final String LIST_OF_DEVICES_STATE = "//div[@class='status']"; + private static final String LIST_OF_DEVICES_PROFILE = "//mat-cell[contains(@class,'deviceProfileName')]"; + private static final String MAKE_DEVICE_PUBLIC_BTN = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'share')]/parent::button"; + private static final String DEVICE_IS_PUBLIC_CHECKBOX = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'check_box')]"; + private static final String MAKE_DEVICE_PUBLIC_BTN_DETAILS_TAB = "//span[contains(text(),'Make device public')]/parent::button"; + private static final String MAKE_DEVICE_PRIVATE_BTN = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'reply')]/parent::button"; + private static final String DEVICE_IS_PRIVATE_CHECKBOX = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'check_box_outline_blank')]"; + private static final String MAKE_DEVICE_PRIVATE_BTN_DETAILS_TAB = "//span[contains(text(),'Make device private')]/parent::button"; public WebElement device(String deviceName) { return waitUntilElementToBeClickable(String.format(DEVICE, deviceName)); @@ -82,8 +95,8 @@ public class DevicePageElements extends OtherPageElementsHelper { return waitUntilElementToBeClickable(CLOSE_DEVICE_DETAILS_VIEW); } - public WebElement submitAssignToCustomerBtn() { - return waitUntilElementToBeClickable(SUBMIT_ASSIGN_TO_CUSTOMER_BTN); + public WebElement submitBtn() { + return waitUntilElementToBeClickable(SUBMIT_BTN); } public WebElement addDeviceBtn() { @@ -177,4 +190,48 @@ public class DevicePageElements extends OtherPageElementsHelper { public WebElement assignMarkedDeviceBtn() { return waitUntilVisibilityOfElementLocated(ASSIGN_MARKED_DEVICE_BTN); } + + public WebElement filterBtn() { + return waitUntilElementToBeClickable(FILTER_BTN); + } + + public WebElement deviceProfileField() { + return waitUntilElementToBeClickable(DEVICE_PROFILE_FIELD); + } + + public WebElement deviceStateSelect() { + return waitUntilElementToBeClickable(DEVICE_STATE_SELECT); + } + + public List listOfDevicesState() { + return waitUntilVisibilityOfElementsLocated(LIST_OF_DEVICES_STATE); + } + + public List listOfDevicesProfile() { + return waitUntilVisibilityOfElementsLocated(LIST_OF_DEVICES_PROFILE); + } + + public WebElement makeDevicePublicBtn(String deviceName) { + return waitUntilElementToBeClickable(String.format(MAKE_DEVICE_PUBLIC_BTN, deviceName)); + } + + public WebElement deviceIsPublicCheckbox(String deviceName) { + return waitUntilVisibilityOfElementLocated(String.format(DEVICE_IS_PUBLIC_CHECKBOX, deviceName)); + } + + public WebElement makeDevicePublicBtnDetailsTab() { + return waitUntilElementToBeClickable(MAKE_DEVICE_PUBLIC_BTN_DETAILS_TAB); + } + + public WebElement makeDevicePrivateBtn(String deviceName) { + return waitUntilElementToBeClickable(String.format(MAKE_DEVICE_PRIVATE_BTN, deviceName)); + } + + public WebElement deviceIsPrivateCheckbox(String deviceName) { + return waitUntilVisibilityOfElementLocated(String.format(DEVICE_IS_PRIVATE_CHECKBOX, deviceName)); + } + + public WebElement makeDevicePrivateBtnDetailsTab() { + return waitUntilElementToBeClickable(MAKE_DEVICE_PRIVATE_BTN_DETAILS_TAB); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java index 3239bb5484..599b9f104f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java @@ -35,7 +35,7 @@ public class DevicePageHelper extends DevicePageElements { public void assignToCustomer(String customerTitle) { chooseCustomerForAssignField().click(); entityFromDropdown(customerTitle).click(); - submitAssignToCustomerBtn().click(); + submitBtn().click(); } public void openCreateDeviceView() { @@ -101,4 +101,46 @@ public class DevicePageHelper extends DevicePageElements { deleteSelectedBtn().click(); warningPopUpYesBtn().click(); } + + public void filterDeviceByDeviceProfile(String deviceProfileTitle) { + clearProfileFieldBtn().click(); + entityFromDropdown(deviceProfileTitle).click(); + submitBtn().click(); + } + + public void filterDeviceByState(String state) { + deviceStateSelect().click(); + entityFromDropdown(" " + state + " ").click(); + sleep(2); //wait until the action is counted + submitBtn().click(); + } + + public void filterDeviceByDeviceProfileAndState(String deviceProfileTitle, String state) { + clearProfileFieldBtn().click(); + entityFromDropdown(deviceProfileTitle).click(); + deviceStateSelect().click(); + entityFromDropdown(" " + state + " ").click(); + sleep(2); //wait until the action is counted + submitBtn().click(); + } + + public void makeDevicePublicByRightSideBtn(String deviceName) { + makeDevicePublicBtn(deviceName).click(); + warningPopUpYesBtn().click(); + } + + public void makeDevicePublicFromDetailsTab() { + makeDevicePublicBtnDetailsTab().click(); + warningPopUpYesBtn().click(); + } + + public void makeDevicePrivateByRightSideBtn(String deviceName) { + makeDevicePrivateBtn(deviceName).click(); + warningPopUpYesBtn().click(); + } + + public void makeDevicePrivateFromDetailsTab() { + makeDevicePrivateBtnDetailsTab().click(); + warningPopUpYesBtn().click(); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AbstractAssignTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AbstractAssignTest.java similarity index 98% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AbstractAssignTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AbstractAssignTest.java index 1d13d16f4b..29b207534d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AbstractAssignTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AbstractAssignTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.assignee; +package org.thingsboard.server.msa.ui.tests.alarmassignee; import io.qameta.allure.Epic; import org.testng.annotations.AfterClass; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignDetailsTabAssignTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignDetailsTabAssignTest.java similarity index 97% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignDetailsTabAssignTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignDetailsTabAssignTest.java index 9f90dddc90..3b80af3ffc 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignDetailsTabAssignTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignDetailsTabAssignTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.assignee; +package org.thingsboard.server.msa.ui.tests.alarmassignee; import io.qameta.allure.Description; import io.qameta.allure.Feature; @@ -161,15 +161,13 @@ public class AssignDetailsTabAssignTest extends AbstractAssignTest { } @Description("Search by name") - @Test(groups = "broken") + @Test public void searchByName() { sideBarMenuView.goToDevicesPage(); devicePage.openDeviceAlarms(deviceName); alarmPage.searchAlarm(alarmType, userName); - alarmPage.setUsers(); - assertThat(alarmPage.getUsers()).hasSize(1).as("Search result contains search input").contains(userName); - alarmPage.assignUsers().forEach(this::assertIsDisplayed); + assertIsDisplayed(alarmPage.noUsersFoundMessage()); } @Description("Assign alarm to yourself from details of alarm") diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignDetailsTabFromCustomerAssignTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignDetailsTabFromCustomerAssignTest.java similarity index 99% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignDetailsTabFromCustomerAssignTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignDetailsTabFromCustomerAssignTest.java index 301a61fdf4..a468194f27 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignDetailsTabFromCustomerAssignTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignDetailsTabFromCustomerAssignTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.assignee; +package org.thingsboard.server.msa.ui.tests.alarmassignee; import io.qameta.allure.Description; import io.qameta.allure.Feature; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignFromAlarmWidgetTest.java similarity index 93% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignFromAlarmWidgetTest.java index 5a2d6cd0c5..57fe5ec745 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/alarmassignee/AssignFromAlarmWidgetTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.assignee; +package org.thingsboard.server.msa.ui.tests.alarmassignee; import io.qameta.allure.Description; import io.qameta.allure.Feature; @@ -56,7 +56,7 @@ public class AssignFromAlarmWidgetTest extends AbstractAssignTest { createWidgetPopup.addAliasBtn().click(); createWidgetPopup.addWidgetBtn().click(); dashboardPage.increaseSizeOfTheWidget(); - dashboardPage.doneBtn().click(); + dashboardPage.saveBtn().click(); } @AfterClass @@ -135,12 +135,10 @@ public class AssignFromAlarmWidgetTest extends AbstractAssignTest { } @Description("Search by name") - @Test(groups = "broken") + @Test public void searchByName() { alarmWidget.searchAlarm(alarmType, userName); - alarmWidget.setUsers(); - assertThat(alarmWidget.getUsers()).hasSize(1).as("Search result contains search input").contains(userName); - alarmWidget.assignUsers().forEach(this::assertIsDisplayed); + assertIsDisplayed(alarmWidget.noUsersFoundMessage()); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java index c59ccd62f4..c367aa869d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java @@ -207,9 +207,9 @@ public class CreateCustomerTest extends AbstractDriverBaseTest { sideBarMenuView.customerBtn().click(); customerPage.plusBtn().click(); - customerPage.titleFieldAddEntityView().sendKeys(customerName); - customerPage.phoneNumberAddEntityView().sendKeys(number); - customerPage.phoneNumberAddEntityView().sendKeys(Keys.CONTROL + "A" + Keys.BACK_SPACE); + customerPage.addCustomerViewEnterName(customerName); + customerPage.enterText(customerPage.phoneNumberAddEntityView(), number); + customerPage.clearInputField(customerPage.phoneNumberAddEntityView()); customerPage.addBtnC().click(); this.customerName = customerName; customerPage.entity(customerName).click(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java index 47d950c312..c807a6a505 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java @@ -204,31 +204,32 @@ public class CustomerEditMenuTest extends AbstractDriverBaseTest { @Epic("Customers smoke tests") @Feature("Edit customer") - @Test(priority = 20, groups = "smoke") + @Test(priority = 20, groups = { "smoke", "broken" }) @Description("Assigned dashboard without hide") public void assignedDashboardWithoutHide() { String customerName = ENTITY_NAME + random(); + String dashboardName = "Firmware"; testRestClient.postCustomer(defaultCustomerPrototype(customerName)); this.customerName = customerName; sideBarMenuView.customerBtn().click(); customerPage.manageCustomersDashboardsBtn(customerName).click(); - customerPage.assignedDashboard(); + customerPage.assignedDashboard(dashboardName); sideBarMenuView.customerBtn().click(); customerPage.entity(customerName).click(); jsClick(customerPage.editPencilBtn()); - customerPage.chooseDashboard(customerPage.getDashboard()); - customerPage.hideHomeDashboardToolbarCheckbox().click(); + customerPage.chooseDashboard(dashboardName); + customerPage.disableHideHomeDashboardToolbar(); customerPage.doneBtnEditView().click(); + customerPage.waitUntilDashboardFieldToBeNotEmpty(); customerPage.setDashboardFromView(); customerPage.closeEntityViewBtn().click(); jsClick(customerPage.manageCustomersUserBtn(customerName)); customerPage.createCustomersUser(); jsClick(customerPage.userLoginBtn()); - Assert.assertNotNull(customerPage.usersWidget()); Assert.assertTrue(customerPage.usersWidget().isDisplayed()); - Assert.assertEquals(customerPage.getDashboard(), customerPage.getDashboardFromView()); + Assert.assertEquals(dashboardName, customerPage.getDashboardFromView()); Assert.assertNotNull(customerPage.stateController()); Assert.assertNotNull(customerPage.filterBtn()); Assert.assertNotNull(customerPage.timeBtn()); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java index 5b4f1c0685..d173de961e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java @@ -45,9 +45,5 @@ abstract public class AbstractDeviceTest extends AbstractDriverBaseTest { public void delete() { deleteDeviceByName(deviceName); deviceName = null; - if (deviceProfileTitle != null) { - deleteDeviceProfileByTitle(deviceProfileTitle); - deviceProfileTitle = null; - } } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java index 8c0a45aa03..c4246e6df5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java @@ -34,6 +34,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; +import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; @Feature("Assign to customer") public class AssignToCustomerTest extends AbstractDeviceTest { @@ -58,7 +59,7 @@ public class AssignToCustomerTest extends AbstractDeviceTest { @AfterClass public void deleteCustomer() { deleteCustomerById(customerId); - deleteCustomerByName("Public"); + deleteCustomerByName(PUBLIC_CUSTOMER_NAME); deleteDeviceByName(device1.getName()); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java index 7f13614b37..64b965ac58 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.msa.ui.tests.devicessmoke; import io.qameta.allure.Description; import io.qameta.allure.Feature; +import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.msa.ui.pages.ProfilesPageElements; @@ -33,6 +34,16 @@ import static org.thingsboard.server.msa.ui.utils.Const.SAME_NAME_WARNING_DEVICE @Feature("Create device") public class CreateDeviceTest extends AbstractDeviceTest { + @AfterMethod + public void delete() { + deleteDeviceByName(deviceName); + deviceName = null; + if (deviceProfileTitle != null) { + deleteDeviceProfileByTitle(deviceProfileTitle); + deviceProfileTitle = null; + } + } + @Test(groups = "smoke") @Description("Add device after specifying the name (text/numbers /special characters)") public void createDevice() { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/DeviceFilterTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/DeviceFilterTest.java new file mode 100644 index 0000000000..89f4e91f67 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/DeviceFilterTest.java @@ -0,0 +1,105 @@ +/** + * 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. + */ +package org.thingsboard.server.msa.ui.tests.devicessmoke; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.msa.ui.utils.DataProviderCredential; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; + +@Feature("Filter devices (By device profile and state)") +public class DeviceFilterTest extends AbstractDeviceTest { + + private String activeDeviceName; + private String deviceWithProfileName; + private String activeDeviceWithProfileName; + + @BeforeClass + public void createTestEntities() { + DeviceProfile deviceProfile = testRestClient.postDeviceProfile(EntityPrototypes.defaultDeviceProfile(ENTITY_NAME + random())); + Device deviceWithProfile = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random(), deviceProfile.getId())); + Device activeDevice = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())); + Device activeDeviceWithProfile = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random(), deviceProfile.getId())); + + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(activeDevice.getId()); + DeviceCredentials deviceCredentials1 = testRestClient.getDeviceCredentialsByDeviceId(activeDeviceWithProfile.getId()); + testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), JacksonUtil.toJsonNode(createPayload().toString())); + testRestClient.postTelemetry(deviceCredentials1.getCredentialsId(), JacksonUtil.toJsonNode(createPayload().toString())); + + deviceProfileTitle = deviceProfile.getName(); + deviceWithProfileName = deviceWithProfile.getName(); + activeDeviceName = activeDevice.getName(); + activeDeviceWithProfileName = activeDeviceWithProfile.getName(); + } + + @AfterClass + public void deleteTestEntities() { + deleteDevicesByName(List.of(deviceWithProfileName, activeDeviceName, activeDeviceWithProfileName)); + deleteDeviceProfileByTitle(deviceProfileTitle); + } + + @Test(groups = "smoke") + @Description("Filter by device profile") + public void filterDevicesByProfile() { + sideBarMenuView.goToDevicesPage(); + devicePage.filterBtn().click(); + devicePage.filterDeviceByDeviceProfile(deviceProfileTitle); + + devicePage.listOfDevicesProfile().forEach(d -> assertThat(d.getText()) + .as("There are only devices with the selected profile(%s) on the page", deviceProfileTitle) + .isEqualTo(deviceProfileTitle)); + } + + @Test(groups = "smoke", dataProviderClass = DataProviderCredential.class, dataProvider = "filterData") + @Description("Filter by state") + public void filterDevicesByState(String state) { + sideBarMenuView.goToDevicesPage(); + devicePage.filterBtn().click(); + devicePage.filterDeviceByState(state); + + devicePage.listOfDevicesState().forEach(d -> assertThat(d.getText()) + .as("There are only devices with '%s' state on the page", state) + .isEqualTo(state)); + } + + @Test(groups = "smoke", dataProviderClass = DataProviderCredential.class, dataProvider = "filterData") + @Description("Filter device by device profile and state") + public void filterDevicesByDeviceProfileAndState(String state) { + sideBarMenuView.goToDevicesPage(); + devicePage.filterBtn().click(); + devicePage.filterDeviceByDeviceProfileAndState(deviceProfileTitle, state); + + devicePage.listOfDevicesProfile().forEach(d -> assertThat(d.getText()) + .as("There are only devices with the selected profile(%s) on the page", deviceProfileTitle) + .isEqualTo(deviceProfileTitle)); + devicePage.listOfDevicesState().forEach(d -> assertThat(d.getText()) + .as("There are only devices with '%s' state on the page", state) + .isEqualTo(state)); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePrivateTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePrivateTest.java new file mode 100644 index 0000000000..df14547bda --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePrivateTest.java @@ -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. + */ +package org.thingsboard.server.msa.ui.tests.devicessmoke; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import org.openqa.selenium.WebElement; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; +import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; + +@Feature("Make device private") +public class MakeDevicePrivateTest extends AbstractDeviceTest { + + private CustomerPageHelper customerPage; + + @BeforeMethod + public void createPublicDevice() { + customerPage = new CustomerPageHelper(driver); + Device device = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())); + testRestClient.setDevicePublic(device.getId()); + deviceName = device.getName(); + } + + @AfterClass + public void deletePublicCustomer() { + deleteCustomerByName(PUBLIC_CUSTOMER_NAME); + } + + @Test(groups = "smoke") + @Description("Make device private by right side btn") + public void makeDevicePrivateByRightSideBtn() { + sideBarMenuView.goToDevicesPage(); + devicePage.makeDevicePrivateByRightSideBtn(deviceName); + WebElement customerInColumn = devicePage.deviceCustomerOnPage(deviceName); + assertIsDisplayed(devicePage.deviceIsPrivateCheckbox(deviceName)); + assertInvisibilityOfElement(customerInColumn); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + devicePage.assertEntityIsNotPresent(deviceName); + } + + @Test(groups = "smoke") + @Description("Make device public by btn on details tab") + public void makeDevicePrivateFromDetailsTab() { + sideBarMenuView.goToDevicesPage(); + devicePage.device(deviceName).click(); + WebElement customerInColumn = devicePage.deviceCustomerOnPage(deviceName); + devicePage.makeDevicePrivateFromDetailsTab(); + devicePage.closeDeviceDetailsViewBtn().click(); + assertIsDisplayed(devicePage.deviceIsPrivateCheckbox(deviceName)); + assertInvisibilityOfElement(customerInColumn); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + devicePage.assertEntityIsNotPresent(deviceName); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePublicTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePublicTest.java new file mode 100644 index 0000000000..ad5b379437 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePublicTest.java @@ -0,0 +1,127 @@ +/** + * 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. + */ +package org.thingsboard.server.msa.ui.tests.devicessmoke; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; +import org.thingsboard.server.msa.ui.tabs.AssignDeviceTabHelper; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; +import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; + +@Feature("Make device public") +public class MakeDevicePublicTest extends AbstractDeviceTest { + + private CustomerPageHelper customerPage; + private AssignDeviceTabHelper assignDeviceTab; + private String deviceName1; + + @BeforeClass + public void createFirstDevice() { + customerPage = new CustomerPageHelper(driver); + assignDeviceTab = new AssignDeviceTabHelper(driver); + + deviceName1 = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())).getName(); + } + + @AfterClass + public void cleanUp() { + deleteCustomerByName(PUBLIC_CUSTOMER_NAME); + deleteDeviceByName(deviceName1); + } + + @BeforeMethod + public void createSecondDevice() { + deviceName = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())).getName(); + } + + @Test(groups = "smoke", priority = 10) + @Description("Make device public by right side btn") + public void makeDevicePublicByRightSideBtn() { + sideBarMenuView.goToDevicesPage(); + devicePage.makeDevicePublicByRightSideBtn(deviceName); + + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer in customer column is Public customer") + .isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke", priority = 10) + @Description("Make device public by btn on details tab") + public void makeDevicePublicFromDetailsTab() { + sideBarMenuView.goToDevicesPage(); + devicePage.device(deviceName).click(); + devicePage.makeDevicePublicFromDetailsTab(); + devicePage.closeDeviceDetailsViewBtn().click(); + + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer in customer column is Public customer") + .isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke", priority = 20) + @Description("Make device public by assign to public customer") + public void makeDevicePublicByAssignToPublicCustomer() { + sideBarMenuView.goToDevicesPage(); + devicePage.assignBtn(deviceName).click(); + assignDeviceTab.assignOnCustomer(PUBLIC_CUSTOMER_NAME); + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()).isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke", priority = 20) + @Description("Make several devices public by assign to public customer") + public void makePublicSeveralDevicesByAssignOnPublicCustomer() { + sideBarMenuView.goToDevicesPage(); + devicePage.assignSelectedDevices(deviceName, deviceName1); + assignDeviceTab.assignOnCustomer(PUBLIC_CUSTOMER_NAME); + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer in customer column is Public customer") + .isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + assertIsDisplayed(devicePage.device(deviceName1)); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java index 28bdac556f..892b4af9bc 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java @@ -45,4 +45,7 @@ public class Const { public static final String PHONE_NUMBER_ERROR_MESSAGE = "Phone number is invalid or not possible"; public static final String NAME_IS_REQUIRED_MESSAGE = "Name is required."; public static final String DEVICE_PROFILE_IS_REQUIRED_MESSAGE = "Device profile is required"; + public static final String DEVICE_ACTIVE_STATE = "Active"; + public static final String DEVICE_INACTIVE_STATE = "Inactive"; + public static final String PUBLIC_CUSTOMER_NAME = "Public"; } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java index e7ee242cab..218751f709 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java @@ -21,6 +21,8 @@ import org.testng.annotations.DataProvider; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.getRandomNumber; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.getRandomSymbol; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.DEVICE_ACTIVE_STATE; +import static org.thingsboard.server.msa.ui.utils.Const.DEVICE_INACTIVE_STATE; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; public class DataProviderCredential { @@ -166,4 +168,12 @@ public class DataProviderCredential { {label, newLabel, label + newLabel}, {label, Keys.CONTROL + "A" + Keys.BACK_SPACE, ""}}; } + + @DataProvider(name = "filterData") + public static Object[][] getFilterData() { + return new Object[][]{ + {DEVICE_ACTIVE_STATE}, + {DEVICE_INACTIVE_STATE} + }; + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java index ec66442bd2..252061fca8 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.rule.RuleChain; @@ -227,6 +228,14 @@ public class EntityPrototypes { return device; } + public static Device defaultDevicePrototype(String name, DeviceProfileId deviceProfileId) { + Device device = new Device(); + device.setName(name + RandomStringUtils.randomAlphanumeric(7)); + device.setType("DEFAULT"); + device.setDeviceProfileId(deviceProfileId); + return device; + } + public static Asset defaultAssetPrototype(String name, CustomerId id) { Asset asset = new Asset(); asset.setName(name + RandomStringUtils.randomAlphanumeric(7)); diff --git a/msa/black-box-tests/src/test/resources/assignee.xml b/msa/black-box-tests/src/test/resources/alarmAssignee.xml similarity index 94% rename from msa/black-box-tests/src/test/resources/assignee.xml rename to msa/black-box-tests/src/test/resources/alarmAssignee.xml index 49fe1aa195..169a1edf32 100644 --- a/msa/black-box-tests/src/test/resources/assignee.xml +++ b/msa/black-box-tests/src/test/resources/alarmAssignee.xml @@ -18,7 +18,7 @@ --> - + @@ -29,7 +29,7 @@ - + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/all.xml b/msa/black-box-tests/src/test/resources/all.xml index 95248a6fe6..7bf6c85546 100644 --- a/msa/black-box-tests/src/test/resources/all.xml +++ b/msa/black-box-tests/src/test/resources/all.xml @@ -77,4 +77,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/uiTests.xml b/msa/black-box-tests/src/test/resources/uiTests.xml index 9de384d15c..e9bea8b1ef 100644 --- a/msa/black-box-tests/src/test/resources/uiTests.xml +++ b/msa/black-box-tests/src/test/resources/uiTests.xml @@ -72,4 +72,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 352f94e091..9d5ad35388 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -192,7 +192,14 @@ metrics: # Metrics percentiles returned by actuator for timer metrics. List of double values (divided by ,). percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" +management: + endpoints: + web: + exposure: + # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). + include: '${METRICS_ENDPOINTS_EXPOSE:info}' + service: type: "${TB_SERVICE_TYPE:tb-vc-executor}" # Unique id for this service (autogenerated if empty) - id: "${TB_SERVICE_ID:}" \ No newline at end of file + id: "${TB_SERVICE_ID:}" diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java index e243f66633..fa88fa0254 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java @@ -35,7 +35,9 @@ import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Promise; +import lombok.extern.slf4j.Slf4j; +@Slf4j final class MqttChannelHandler extends SimpleChannelInboundHandler { private final MqttClientImpl client; @@ -48,31 +50,36 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler @Override protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception { - switch (msg.fixedHeader().messageType()) { - case CONNACK: - handleConack(ctx.channel(), (MqttConnAckMessage) msg); - break; - case SUBACK: - handleSubAck((MqttSubAckMessage) msg); - break; - case PUBLISH: - handlePublish(ctx.channel(), (MqttPublishMessage) msg); - break; - case UNSUBACK: - handleUnsuback((MqttUnsubAckMessage) msg); - break; - case PUBACK: - handlePuback((MqttPubAckMessage) msg); - break; - case PUBREC: - handlePubrec(ctx.channel(), msg); - break; - case PUBREL: - handlePubrel(ctx.channel(), msg); - break; - case PUBCOMP: - handlePubcomp(msg); - break; + if (msg.decoderResult().isSuccess()) { + switch (msg.fixedHeader().messageType()) { + case CONNACK: + handleConack(ctx.channel(), (MqttConnAckMessage) msg); + break; + case SUBACK: + handleSubAck((MqttSubAckMessage) msg); + break; + case PUBLISH: + handlePublish(ctx.channel(), (MqttPublishMessage) msg); + break; + case UNSUBACK: + handleUnsuback((MqttUnsubAckMessage) msg); + break; + case PUBACK: + handlePuback((MqttPubAckMessage) msg); + break; + case PUBREC: + handlePubrec(ctx.channel(), msg); + break; + case PUBREL: + handlePubrel(ctx.channel(), msg); + break; + case PUBCOMP: + handlePubcomp(msg); + break; + } + } else { + log.error("[{}] Message decoding failed: {}", client.getClientConfig().getClientId(), msg.decoderResult().cause().getMessage()); + ctx.close(); } } diff --git a/pom.xml b/pom.xml index 98b2e1529a..59d1eff5d3 100755 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 2.9.0 2.3.30 1.6.2 - 4.2.0 + 5.5.0 3.8.1 3.21.9 1.42.1 @@ -111,7 +111,7 @@ 1.11.747 1.105.0 2.1.0 - 3.2.0 + 3.6.7 1.5.0 1.5.4 1.9.4 @@ -151,6 +151,7 @@ 2.12.0 1.12.1 6.4.2 + 1.34.1 @@ -1836,6 +1837,16 @@ com.microsoft.azure azure-servicebus ${azure-servicebus.version} + + + com.nimbusds + content-type + + + org.ow2.asm + asm + + org.passay @@ -2007,6 +2018,11 @@ oshi-core ${oshi.version} + + com.google.oauth-client + google-oauth-client + ${google-oauth-client.version} + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java index 9985446dd3..f740aebf4a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java @@ -43,19 +43,18 @@ public class TbMathArgumentValue { throw new RuntimeException(error); } - public static TbMathArgumentValue fromMessageBody(TbMathArgument arg, Optional jsonNodeOpt) { - String key = arg.getKey(); + public static TbMathArgumentValue fromMessageBody(TbMathArgument arg, String argKey, Optional jsonNodeOpt) { Double defaultValue = arg.getDefaultValue(); if (jsonNodeOpt.isEmpty()) { return defaultOrThrow(defaultValue, "Message body is empty!"); } var json = jsonNodeOpt.get(); - if (!json.has(key)) { - return defaultOrThrow(defaultValue, "Message body has no '" + key + "'!"); + if (!json.has(argKey)) { + return defaultOrThrow(defaultValue, "Message body has no '" + argKey + "'!"); } - JsonNode valueNode = json.get(key); + JsonNode valueNode = json.get(argKey); if (valueNode.isNull()) { - return defaultOrThrow(defaultValue, "Message body has null '" + key + "'!"); + return defaultOrThrow(defaultValue, "Message body has null '" + argKey + "'!"); } double value; if (valueNode.isNumber()) { @@ -69,7 +68,7 @@ public class TbMathArgumentValue { throw new RuntimeException("Can't convert value '" + valueNode.asText() + "' to double!"); } } else { - return defaultOrThrow(defaultValue, "Message value is empty for '" + key + "'!"); + return defaultOrThrow(defaultValue, "Message value is empty for '" + argKey + "'!"); } } else { throw new RuntimeException("Can't convert value '" + valueNode.toString() + "' to double!"); @@ -77,15 +76,14 @@ public class TbMathArgumentValue { return new TbMathArgumentValue(value); } - public static TbMathArgumentValue fromMessageMetadata(TbMathArgument arg, TbMsgMetaData metaData) { - String key = arg.getKey(); + public static TbMathArgumentValue fromMessageMetadata(TbMathArgument arg, String argKey, TbMsgMetaData metaData) { Double defaultValue = arg.getDefaultValue(); if (metaData == null) { return defaultOrThrow(defaultValue, "Message metadata is empty!"); } - var value = metaData.getValue(key); + var value = metaData.getValue(argKey); if (StringUtils.isEmpty(value)) { - return defaultOrThrow(defaultValue, "Message metadata has no '" + key + "'!"); + return defaultOrThrow(defaultValue, "Message metadata has no '" + argKey + "'!"); } return fromString(value); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index eff3917c14..33692decc2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -51,6 +51,8 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; + @SuppressWarnings("UnstableApiUsage") @Slf4j @RuleNode( @@ -121,7 +123,7 @@ public class TbMathNode implements TbNode { var argumentValues = Futures.allAsList(arguments.stream() .map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList())); ListenableFuture resultMsgFuture = Futures.transformAsync(argumentValues, args -> - updateMsgAndDb(ctx, msg, msgBodyOpt, calculateResult(ctx, msg, args)), ctx.getDbCallbackExecutor()); + updateMsgAndDb(ctx, msg, msgBodyOpt, calculateResult(args)), ctx.getDbCallbackExecutor()); DonAsynchron.withCallback(resultMsgFuture, resultMsg -> { try { ctx.tellSuccess(resultMsg); @@ -155,17 +157,18 @@ public class TbMathNode implements TbNode { private ListenableFuture updateMsgAndDb(TbContext ctx, TbMsg msg, Optional msgBodyOpt, double result) { TbMathResult mathResultDef = config.getResult(); + String mathResultKey = getKeyFromTemplate(msg, mathResultDef.getType(), mathResultDef.getKey()); switch (mathResultDef.getType()) { case MESSAGE_BODY: - return Futures.immediateFuture(addToBody(msg, mathResultDef, msgBodyOpt, result)); + return Futures.immediateFuture(addToBody(msg, mathResultDef, mathResultKey, msgBodyOpt, result)); case MESSAGE_METADATA: - return Futures.immediateFuture(addToMeta(msg, mathResultDef, result)); + return Futures.immediateFuture(addToMeta(msg, mathResultDef, mathResultKey, result)); case ATTRIBUTE: ListenableFuture attrSave = saveAttribute(ctx, msg, result, mathResultDef); - return Futures.transform(attrSave, attr -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor()); + return Futures.transform(attrSave, attr -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef, mathResultKey), ctx.getDbCallbackExecutor()); case TIME_SERIES: ListenableFuture tsSave = saveTimeSeries(ctx, msg, result, mathResultDef); - return Futures.transform(tsSave, ts -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor()); + return Futures.transform(tsSave, ts -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef, mathResultKey), ctx.getDbCallbackExecutor()); default: throw new RuntimeException("Result type is not supported: " + mathResultDef.getType() + "!"); } @@ -180,7 +183,7 @@ public class TbMathNode implements TbNode { private ListenableFuture saveAttribute(TbContext ctx, TbMsg msg, double result, TbMathResult mathResultDef) { String attributeScope = getAttributeScope(mathResultDef.getAttributeScope()); if (isIntegerResult(mathResultDef, config.getOperation())) { - var value = toIntValue(mathResultDef, result); + var value = toIntValue(result); return ctx.getTelemetryService().saveAttrAndNotify( ctx.getTenantId(), msg.getOriginator(), attributeScope, mathResultDef.getKey(), value); } else { @@ -194,7 +197,7 @@ public class TbMathNode implements TbNode { return function.isIntegerResult() || mathResultDef.getResultValuePrecision() == 0; } - private long toIntValue(TbMathResult mathResultDef, double value) { + private long toIntValue(double value) { return (long) value; } @@ -217,38 +220,38 @@ public class TbMathNode implements TbNode { return msgBodyOpt; } - private TbMsg addToBodyAndMeta(TbMsg msg, Optional msgBodyOpt, double result, TbMathResult mathResultDef) { + private TbMsg addToBodyAndMeta(TbMsg msg, Optional msgBodyOpt, double result, TbMathResult mathResultDef, String mathResultKey) { TbMsg tmpMsg = msg; if (mathResultDef.isAddToBody()) { - tmpMsg = addToBody(tmpMsg, mathResultDef, msgBodyOpt, result); + tmpMsg = addToBody(tmpMsg, mathResultDef, mathResultKey, msgBodyOpt, result); } if (mathResultDef.isAddToMetadata()) { - tmpMsg = addToMeta(tmpMsg, mathResultDef, result); + tmpMsg = addToMeta(tmpMsg, mathResultDef, mathResultKey, result); } return tmpMsg; } - private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, Optional msgBodyOpt, double result) { + private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, String mathResultKey, Optional msgBodyOpt, double result) { ObjectNode body = msgBodyOpt.get(); if (isIntegerResult(mathResultDef, config.getOperation())) { - body.put(mathResultDef.getKey(), toIntValue(mathResultDef, result)); + body.put(mathResultKey, toIntValue(result)); } else { - body.put(mathResultDef.getKey(), toDoubleValue(mathResultDef, result)); + body.put(mathResultKey, toDoubleValue(mathResultDef, result)); } return TbMsg.transformMsgData(msg, JacksonUtil.toString(body)); } - private TbMsg addToMeta(TbMsg msg, TbMathResult mathResultDef, double result) { + private TbMsg addToMeta(TbMsg msg, TbMathResult mathResultDef, String mathResultKey, double result) { var md = msg.getMetaData(); if (isIntegerResult(mathResultDef, config.getOperation())) { - md.putValue(mathResultDef.getKey(), Long.toString(toIntValue(mathResultDef, result))); + md.putValue(mathResultKey, Long.toString(toIntValue(result))); } else { - md.putValue(mathResultDef.getKey(), Double.toString(toDoubleValue(mathResultDef, result))); + md.putValue(mathResultKey, Double.toString(toDoubleValue(mathResultDef, result))); } return TbMsg.transformMsg(msg, md); } - private double calculateResult(TbContext ctx, TbMsg msg, List args) { + private double calculateResult(List args) { switch (config.getOperation()) { case ADD: return apply(args.get(0), args.get(1), Double::sum); @@ -345,21 +348,22 @@ public class TbMathNode implements TbNode { } private ListenableFuture resolveArguments(TbContext ctx, TbMsg msg, Optional msgBodyOpt, TbMathArgument arg) { + String argKey = getKeyFromTemplate(msg, arg.getType(), arg.getKey()); switch (arg.getType()) { case CONSTANT: return Futures.immediateFuture(TbMathArgumentValue.constant(arg)); case MESSAGE_BODY: - return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg, msgBodyOpt)); + return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg, argKey, msgBodyOpt)); case MESSAGE_METADATA: - return Futures.immediateFuture(TbMathArgumentValue.fromMessageMetadata(arg, msg.getMetaData())); + return Futures.immediateFuture(TbMathArgumentValue.fromMessageMetadata(arg, argKey, msg.getMetaData())); case ATTRIBUTE: String scope = getAttributeScope(arg.getAttributeScope()); - return Futures.transform(ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, arg.getKey()), - opt -> getTbMathArgumentValue(arg, opt, "Attribute: " + arg.getKey() + " with scope: " + scope + " not found for entity: " + msg.getOriginator()) + return Futures.transform(ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, argKey), + opt -> getTbMathArgumentValue(arg, opt, "Attribute: " + argKey + " with scope: " + scope + " not found for entity: " + msg.getOriginator()) , MoreExecutors.directExecutor()); case TIME_SERIES: - return Futures.transform(ctx.getTimeseriesService().findLatest(ctx.getTenantId(), msg.getOriginator(), arg.getKey()), - opt -> getTbMathArgumentValue(arg, opt, "Time-series: " + arg.getKey() + " not found for entity: " + msg.getOriginator()) + return Futures.transform(ctx.getTimeseriesService().findLatest(ctx.getTenantId(), msg.getOriginator(), argKey), + opt -> getTbMathArgumentValue(arg, opt, "Time-series: " + argKey + " not found for entity: " + msg.getOriginator()) , MoreExecutors.directExecutor()); default: throw new RuntimeException("Unsupported argument type: " + arg.getType() + "!"); @@ -367,6 +371,10 @@ public class TbMathNode implements TbNode { } + private String getKeyFromTemplate(TbMsg msg, TbMathArgumentType type, String keyPattern) { + return CONSTANT.equals(type) ? keyPattern : TbNodeUtils.processPattern(keyPattern, msg); + } + private String getAttributeScope(String attrScope) { return StringUtils.isEmpty(attrScope) ? DataConstants.SERVER_SCOPE : attrScope; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index 7723d6028f..ae7ceade97 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -77,8 +77,10 @@ public class TbSplitArrayMsgNode implements TbNode { ctx.tellFailure(msg, e); } }); - data.forEach(msgNode -> ctx.enqueueForTellNext(TbMsg.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(msgNode)), - TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure)); + data.forEach(msgNode -> { + TbMsg outMsg = TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(msgNode)); + ctx.enqueueForTellNext(outMsg, TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure); + }); } } else { ctx.tellFailure(msg, new RuntimeException("Msg data is not a JSON Array!")); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java index e6b8377f51..eb86fa0843 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.EntityId; import java.util.List; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java index 984d9bfb72..eccc9bb932 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java @@ -31,7 +31,7 @@ public class TbMathArgumentValueTest { public void test_fromMessageBody_then_defaultValue() { TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); tbMathArgument.setDefaultValue(5.0); - TbMathArgumentValue result = TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.ofNullable(JacksonUtil.newObjectNode())); + TbMathArgumentValue result = TbMathArgumentValue.fromMessageBody(tbMathArgument, tbMathArgument.getKey(), Optional.ofNullable(JacksonUtil.newObjectNode())); Assert.assertEquals(5.0, result.getValue(), 0d); } @@ -39,7 +39,7 @@ public class TbMathArgumentValueTest { public void test_fromMessageBody_then_emptyBody() { TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); Throwable thrown = assertThrows(RuntimeException.class, () -> { - TbMathArgumentValue result = TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.empty()); + TbMathArgumentValue result = TbMathArgumentValue.fromMessageBody(tbMathArgument, tbMathArgument.getKey(), Optional.empty()); }); Assert.assertNotNull(thrown.getMessage()); } @@ -47,7 +47,7 @@ public class TbMathArgumentValueTest { @Test public void test_fromMessageBody_then_noKey() { TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); - Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.ofNullable(JacksonUtil.newObjectNode()))); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, tbMathArgument.getKey(), Optional.ofNullable(JacksonUtil.newObjectNode()))); Assert.assertNotNull(thrown.getMessage()); } @@ -58,12 +58,12 @@ public class TbMathArgumentValueTest { msgData.putNull("TestKey"); //null value - Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, tbMathArgument.getKey(), Optional.of(msgData))); Assert.assertNotNull(thrown.getMessage()); //empty value msgData.put("TestKey", ""); - thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, tbMathArgument.getKey(), Optional.of(msgData))); Assert.assertNotNull(thrown.getMessage()); } @@ -74,26 +74,26 @@ public class TbMathArgumentValueTest { msgData.put("TestKey", "Test"); //string value - Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, tbMathArgument.getKey(), Optional.of(msgData))); Assert.assertNotNull(thrown.getMessage()); //object value msgData.set("TestKey", JacksonUtil.newObjectNode()); - thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, tbMathArgument.getKey(), Optional.of(msgData))); Assert.assertNotNull(thrown.getMessage()); } @Test public void test_fromMessageMetadata_then_noKey() { TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); - Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageMetadata(tbMathArgument, new TbMsgMetaData())); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageMetadata(tbMathArgument, tbMathArgument.getKey(), new TbMsgMetaData())); Assert.assertNotNull(thrown.getMessage()); } @Test public void test_fromMessageMetadata_then_valueEmpty() { TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); - Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageMetadata(tbMathArgument, null)); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageMetadata(tbMathArgument, tbMathArgument.getKey(), null)); Assert.assertNotNull(thrown.getMessage()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index 69d1b46dbe..2efc438f8f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -16,7 +16,10 @@ package org.thingsboard.rule.engine.math; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; +import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -26,6 +29,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; @@ -47,7 +51,11 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.Arrays; +import java.util.List; import java.util.Optional; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -57,6 +65,7 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +@Slf4j @RunWith(MockitoJUnitRunner.class) public class TbMathNodeTest { @@ -130,24 +139,52 @@ public class TbMathNodeTest { @Test public void testExp4j() { var node = initNodeWithCustomFunction("2a+3b", - new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), - new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), - new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") + new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "${key1}", 2, false, false, null), + new TbMathArgument("a", TbMathArgumentType.MESSAGE_BODY, "${key2}"), + new TbMathArgument("b", TbMathArgumentType.MESSAGE_BODY, "$[key3]") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("key1", "firstMsgResult"); + metaData.putValue("key2", "argumentA"); + ObjectNode msgNode = JacksonUtil.newObjectNode() + .put("key3", "argumentB").put("argumentA", 2).put("argumentB", 2); + TbMsg msg = TbMsg.newMsg("TEST", originator, metaData, msgNode.toString()); node.onMsg(ctx, msg); - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + ConcurrentMap semaphores = (ConcurrentMap) ReflectionTestUtils.getField(node, "semaphores"); + Assert.assertNotNull(semaphores); + Semaphore originatorSemaphore = semaphores.get(originator); + Assert.assertNotNull(originatorSemaphore); - TbMsg resultMsg = msgCaptor.getValue(); - Assert.assertNotNull(resultMsg); - Assert.assertNotNull(resultMsg.getData()); - var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); - Assert.assertTrue(resultJson.has("result")); - Assert.assertEquals(10, resultJson.get("result").asInt()); + metaData.putValue("key1", "secondMsgResult"); + metaData.putValue("key2", "argumentC"); + msgNode = JacksonUtil.newObjectNode() + .put("key3", "argumentD").put("argumentC", 4).put("argumentD", 3); + msg = TbMsg.newMsg("TEST", originator, metaData, msgNode.toString()); + + node.onMsg(ctx, msg); + + Awaitility.await("Semaphore released").atMost(5, TimeUnit.SECONDS).until(semaphores.get(originator)::tryAcquire); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + Mockito.verify(ctx, Mockito.times(2)).tellSuccess(msgCaptor.capture()); + + List resultMsgs = msgCaptor.getAllValues(); + Assert.assertFalse(resultMsgs.isEmpty()); + Assert.assertEquals(2, resultMsgs.size()); + + for (int i = 0; i < resultMsgs.size(); i++) { + TbMsg outMsg = resultMsgs.get(i); + Assert.assertNotNull(outMsg); + Assert.assertNotNull(outMsg.getData()); + var resultJson = JacksonUtil.toJsonNode(outMsg.getData()); + String resultKey = i == 0 ? "firstMsgResult" : "secondMsgResult"; + Assert.assertTrue(resultJson.has(resultKey)); + Assert.assertEquals(i == 0 ? 10 : 17, resultJson.get(resultKey).asInt()); + } + semaphores.remove(originator); } @Test diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java index 435ec91aaa..67ebf6f6fc 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java @@ -96,6 +96,7 @@ public class TbSplitArrayMsgNodeTest { ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor exceptionCaptor = ArgumentCaptor.forClass(Exception.class); verify(ctx, never()).tellSuccess(any()); + verify(ctx, never()).enqueueForTellNext(any(), anyString(), any(), any()); verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture()); assertThat(exceptionCaptor.getValue()).isInstanceOf(RuntimeException.class); diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 7ea553fe5c..332709e3fa 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 346ec48eae..c8a45c161b 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -73,7 +73,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -93,6 +93,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 4e8167d89d..5de2484860 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 1e0b1ebcd4..ed25d00732 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 9f086bcbc5..f7fa3902b8 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/ui-ngx/package.json b/ui-ngx/package.json index ed096b871a..4112e24fce 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -96,7 +96,7 @@ "schema-inspector": "^2.0.2", "screenfull": "^6.0.2", "split.js": "^1.6.5", - "systemjs": "6.11.0", + "systemjs": "6.14.1", "tinycolor2": "^1.6.0", "tinymce": "~5.10.7", "tooltipster": "^4.2.8", @@ -137,7 +137,7 @@ "@types/raphael": "^2.3.2", "@types/react": "17.0.37", "@types/react-dom": "17.0.11", - "@types/systemjs": "6.1.1", + "@types/systemjs": "6.13.1", "@types/tinycolor2": "^1.4.3", "@types/tooltipster": "^0.0.31", "@typescript-eslint/eslint-plugin": "5.57.0", 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/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts index cd83c3c8c9..d7126df824 100644 --- a/ui-ngx/src/app/core/http/admin.service.ts +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -21,6 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { AdminSettings, AutoCommitSettings, + MailConfigTemplate, FeaturesInfo, JwtSettings, MailServerSettings, @@ -136,4 +137,16 @@ export class AdminService { public getFeaturesInfo(config?: RequestConfig): Observable { return this.http.get('/api/admin/featuresInfo', defaultHttpOptionsFromConfig(config)); } + + public getLoginProcessingUrl(config?: RequestConfig): Observable { + return this.http.get(`/api/admin/mail/oauth2/loginProcessingUrl`, defaultHttpOptionsFromConfig(config)); + } + + public generateAccessToken(config?: RequestConfig): Observable { + return this.http.get(`/api/admin/mail/oauth2/authorize`, defaultHttpOptionsFromConfig(config)); + } + + public getMailConfigTemplate(config?: RequestConfig): Observable> { + return this.http.get>('/api/mail/config/template', defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index ee3e232bba..12f6300b93 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -816,7 +816,8 @@ export class EntityService { ); } - public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[], config?: RequestConfig): Observable> { + public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[], + entityTypes?: EntityType[], config?: RequestConfig): Observable> { if (!types.length) { return of([]); } @@ -832,7 +833,7 @@ export class EntityService { entitiesKeysByQuery$ = of({ attribute: [], timeseries: [], - entityTypes: [], + entityTypes: entityTypes || [], }); } return entitiesKeysByQuery$.pipe( diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 9296f701b0..f7cb8441d1 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -20,8 +20,9 @@ import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; -import { Resource, ResourceInfo } from '@shared/models/resource.models'; +import { Resource, ResourceInfo, ResourceType } from '@shared/models/resource.models'; import { catchError, map, mergeMap } from 'rxjs/operators'; +import { isNotEmptyStr } from '@core/utils'; @Injectable({ providedIn: 'root' @@ -33,15 +34,22 @@ export class ResourceService { } - public getResources(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/resource${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable> { + let url = `/api/resource${pageLink.toQuery()}`; + if (isNotEmptyStr(resourceType)) { + url += `&resourceType=${resourceType}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } public getResource(resourceId: string, config?: RequestConfig): Observable { return this.http.get(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config)); } + public getResourceInfo(resourceId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/resource/info/${resourceId}`, defaultHttpOptionsFromConfig(config)); + } + public downloadResource(resourceId: string): Observable { return this.http.get(`/api/resource/${resourceId}/download`, { responseType: 'arraybuffer', 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 6c36dde958..8355ce2f61 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -29,13 +29,25 @@ import { GridSettings, WidgetLayout } from '@shared/models/dashboard.models'; -import { isDefined, isString, isUndefined } from '@core/utils'; -import { Datasource, DatasourceType, Widget, WidgetConfig, widgetType } from '@app/shared/models/widget.models'; +import { deepClone, isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils'; +import { + DataKey, + Datasource, + datasourcesHasOnlyComparisonAggregation, + DatasourceType, + defaultLegendConfig, + Widget, + WidgetConfig, + 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' @@ -204,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; } @@ -218,16 +247,15 @@ 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; } }); if (type === widgetType.latest) { - widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, this.timeService); + const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService); } if (type === widgetType.alarm) { if (!widgetConfig.alarmFilterConfig) { @@ -308,6 +336,48 @@ 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 = []; + if (widgetTypeDescriptor.type === widgetType.alarm) { + dataKeys.forEach(dataKey => { + const newDataKey = deepClone(dataKey); + newDataKey.funcBody = null; + newDataKey.type = DataKeyType.alarm; + newDatasource.dataKeys.push(newDataKey); + }); + } + } + return newDatasource; + } + private validateAndUpdateState(state: DashboardState) { if (!state.layouts) { state.layouts = this.createDefaultLayouts(); @@ -421,7 +491,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; @@ -494,7 +564,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/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts index 6d664085b1..be0c24a958 100644 --- a/ui-ngx/src/app/core/services/dialog.service.ts +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -29,6 +29,7 @@ import { } from '@shared/components/dialog/material-icons-dialog.component'; import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component'; import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; @Injectable( @@ -78,6 +79,23 @@ export class DialogService { return dialogRef.afterClosed(); } + errorAlert(title: string, message: string, error: any, ok: string = null, fullscreen: boolean = false): Observable { + const dialogConfig: MatDialogConfig = { + disableClose: true, + data: { + title, + message, + error, + ok: ok || this.translate.instant('action.ok') + } + }; + if (fullscreen) { + dialogConfig.panelClass = ['tb-fullscreen-dialog']; + } + const dialogRef = this.dialog.open(ErrorAlertDialogComponent, dialogConfig); + return dialogRef.afterClosed(); + } + colorPicker(color: string): Observable { return this.dialog.open(ColorPickerDialogComponent, { diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index b34cec5eb0..70084153e8 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -27,6 +27,13 @@ import { DOCUMENT } from '@angular/common'; import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { IModulesMap } from '@modules/common/modules-map.models'; +import { TbResourceId } from '@shared/models/id/tb-resource-id'; +import { isObject } from '@core/utils'; +import { AuthService } from '@core/auth/auth.service'; +import { select, Store } from '@ngrx/store'; +import { selectIsAuthenticated } from '@core/auth/auth.selectors'; +import { AppState } from '@core/core.state'; +import { tap } from 'rxjs/operators'; declare const System; @@ -47,9 +54,12 @@ export class ResourcesService { private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; constructor(@Inject(DOCUMENT) private readonly document: any, + protected store: Store, private compiler: Compiler, private http: HttpClient, - private injector: Injector) {} + private injector: Injector) { + this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache()); + } public loadResource(url: string): Observable { if (this.loadedResources[url]) { @@ -62,23 +72,25 @@ export class ResourcesService { fileType = match[1]; } if (!fileType) { - return throwError(new Error(`Unable to detect file type from url: ${url}`)); + return throwError(() => new Error(`Unable to detect file type from url: ${url}`)); } else if (fileType !== 'css' && fileType !== 'js') { - return throwError(new Error(`Unsupported file type: ${fileType}`)); + return throwError(() => new Error(`Unsupported file type: ${fileType}`)); } return this.loadResourceByType(fileType, url); } - public loadFactories(url: string, modulesMap: IModulesMap): Observable { + public loadFactories(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable { + const url = this.getDownloadUrl(resourceId); if (this.loadedModulesAndFactories[url]) { return this.loadedModulesAndFactories[url].asObservable(); } modulesMap.init(); + const meta = this.getMetaInfo(resourceId); const subject = new ReplaySubject(); this.loadedModulesAndFactories[url] = subject; import('@angular/compiler').then( () => { - System.import(url).then( + System.import(url, undefined, meta).then( (module) => { const modules = this.extractNgModules(module); if (modules.length) { @@ -86,7 +98,8 @@ export class ResourcesService { for (const m of modules) { tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m)); } - forkJoin(tasks).subscribe((compiled) => { + forkJoin(tasks).subscribe({ + next: (compiled) => { try { const componentFactories: ComponentFactory[] = []; for (const c of compiled) { @@ -101,38 +114,46 @@ export class ResourcesService { this.loadedModulesAndFactories[url].complete(); } catch (e) { this.loadedModulesAndFactories[url].error(new Error(`Unable to init module from url: ${url}`)); - delete this.loadedModulesAndFactories[url]; } }, - (e) => { + error: (e) => { this.loadedModulesAndFactories[url].error(new Error(`Unable to compile module from url: ${url}`)); - delete this.loadedModulesAndFactories[url]; - }); + } + }); } else { this.loadedModulesAndFactories[url].error(new Error(`Module '${url}' doesn't have default export!`)); - delete this.loadedModulesAndFactories[url]; } }, (e) => { this.loadedModulesAndFactories[url].error(new Error(`Unable to load module from url: ${url}`)); - delete this.loadedModulesAndFactories[url]; } ); } ); - return subject.asObservable(); + return subject.asObservable().pipe( + tap({ + next: () => System.delete(url), + error: () => { + delete this.loadedModulesAndFactories[url]; + System.delete(url); + }, + complete: () => System.delete(url) + }) + ); } - public loadModules(url: string, modulesMap: IModulesMap): Observable[]> { + public loadModules(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable[]> { + const url = this.getDownloadUrl(resourceId); if (this.loadedModules[url]) { return this.loadedModules[url].asObservable(); } modulesMap.init(); + const meta = this.getMetaInfo(resourceId); const subject = new ReplaySubject[]>(); this.loadedModules[url] = subject; import('@angular/compiler').then( () => { - System.import(url).then( + System.import(url, undefined, meta).then( (module) => { try { let modules; @@ -155,31 +176,35 @@ export class ResourcesService { this.loadedModules[url].complete(); } catch (e) { this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`)); - delete this.loadedModules[url]; } }, (e) => { this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`)); - delete this.loadedModules[url]; }); } else { this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`)); - delete this.loadedModules[url]; } } catch (e) { this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); - delete this.loadedModules[url]; } }, (e) => { this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); - delete this.loadedModules[url]; console.error(`Unable to load module from url: ${url}`, e); } ); } ); - return subject.asObservable(); + return subject.asObservable().pipe( + tap({ + next: () => System.delete(url), + error: () => { + delete this.loadedModulesAndFactories[url]; + System.delete(url); + }, + complete: () => System.delete(url) + }) + ); } private extractNgModules(module: any, modules: Type[] = []): Type[] { @@ -246,4 +271,26 @@ export class ResourcesService { this.anchor.appendChild(el); return subject.asObservable(); } + + private getDownloadUrl(resourceId: string | TbResourceId): string { + if (isObject(resourceId)) { + return `/api/resource/js/${(resourceId as TbResourceId).id}/download`; + } + return resourceId as string; + } + + private getMetaInfo(resourceId: string | TbResourceId): object { + if (isObject(resourceId)) { + return { + additionalHeaders: { + 'X-Authorization': `Bearer ${AuthService.getJwtToken()}` + } + }; + } + } + + private clearModulesCache() { + this.loadedModules = {}; + this.loadedModulesAndFactories = {}; + } } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 813127cbc5..f82c6c3072 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -188,6 +188,10 @@ export class UtilsService { public processWidgetException(exception: any): ExceptionData { const data = this.parseException(exception, -6); + if (data.message?.startsWith('NG0')) { + data.message = `${this.translate.instant('widget.widget-template-error')}
+
${this.translate.instant('dialog.error-message-title')}

${data.message}`; + } if (this.widgetEditMode) { const message: WindowMessage = { type: 'widgetException', diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 40172e2667..58f434a77f 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -177,6 +177,7 @@ import * as CopyButtonComponent from '@shared/components/button/copy-button.comp import * as TogglePasswordComponent from '@shared/components/button/toggle-password.component'; import * as ProtobufContentComponent from '@shared/components/protobuf-content.component'; import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component'; +import * as ToggleHeaderComponent from '@shared/components/toggle-header.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; @@ -196,7 +197,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 +207,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'; @@ -300,6 +301,7 @@ import * as QueueFormComponent from '@home/components/queue/queue-form.component import * as AssetProfileComponent from '@home/components/profile/asset-profile.component'; import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component'; import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component'; +import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component'; import { IModulesMap } from '@modules/common/modules-map.models'; @@ -418,6 +420,7 @@ class ModulesMap implements IModulesMap { '@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent, '@shared/components/dashboard-select.component': DashboardSelectComponent, '@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent, + '@shared/components/rule-chain/rule-chain-select.component': RuleChainSelectComponent, '@shared/components/time/datetime-period.component': DatetimePeriodComponent, '@shared/components/time/datetime.component': DatetimeComponent, '@shared/components/time/timezone-select.component': TimezoneSelectComponent, @@ -472,6 +475,7 @@ class ModulesMap implements IModulesMap { '@shared/components/button/toggle-password.component': TogglePasswordComponent, '@shared/components/protobuf-content.component': ProtobufContentComponent, '@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent, + '@shared/components/toggle-header.component': ToggleHeaderComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent, @@ -491,7 +495,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 +505,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, @@ -614,6 +618,13 @@ class ModulesMap implements IModulesMap { for (const moduleId of Object.keys(this.modulesMap)) { System.set('app:' + moduleId, this.modulesMap[moduleId]); } + System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download'); + System.constructor.prototype.fetch = (url, options: RequestInit & {meta?: any}) => { + if (options?.meta?.additionalHeaders) { + options.headers = { ...options.headers, ...options.meta.additionalHeaders }; + } + return fetch(url, options); + }; this.initialized = true; } } 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 41468d096f..28e64738a8 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 @@ -86,7 +86,7 @@ export class AlarmTableConfig extends EntityTableConfig private utilsService: UtilsService, pageMode = false) { super(); - this.loadDataOnInit = false; + this.loadDataOnInit = pageMode; this.tableTitle = ''; this.useTimePageLink = true; this.forAllTimeEnabled = true; 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..5af51031bf 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/entity/add-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html index 4abf910c1a..66dbbba2e3 100644 --- a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ translations.add }}

diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index bce66fc3c1..fbdd0a07da 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { MatDialog } from '@angular/material/dialog'; @@ -32,7 +41,7 @@ import { Subscription } from 'rxjs'; templateUrl: './event-table.component.html', styleUrls: ['./event-table.component.scss'] }) -export class EventTableComponent implements OnInit, AfterViewInit { +export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { @Input() tenantId: string; 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/profile/queue/tenant-profile-queues.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts index 0a5e5dc3ee..ac128b3ec5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts @@ -116,7 +116,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid } writeValue(queues: Array | null): void { - if (queues.length === this.queuesFormArray.length) { + if (queues?.length === this.queuesFormArray.length) { this.queuesFormArray.patchValue(queues, {emitEvent: false}); } else { const queuesControls: Array = []; diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index ce2f086d4d..c2872d4145 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -21,7 +21,7 @@ {{ 'tenant-profile.entities' | translate }} tenant-profile.unlimited
- + tenant-profile.maximum-devices {{ 'tenant-profile.maximum-devices-range' | translate}} + - + tenant-profile.maximum-dashboards {{ 'tenant-profile.maximum-dashboards-range' | translate}} +
- + tenant-profile.maximum-assets {{ 'tenant-profile.maximum-assets-range' | translate}} + - + tenant-profile.maximum-users {{ 'tenant-profile.maximum-users-range' | translate}} +
@@ -80,7 +84,7 @@
- + tenant-profile.maximum-customers {{ 'tenant-profile.maximum-customers-range' | translate}} + - + tenant-profile.maximum-rule-chains {{ 'tenant-profile.maximum-rule-chains-range' | translate}} +
@@ -114,7 +120,7 @@ {{ 'tenant-profile.rule-engine' | translate }} tenant-profile.unlimited
- + tenant-profile.max-r-e-executions {{ 'tenant-profile.max-r-e-executions-range' | translate}} + - + tenant-profile.max-transport-messages {{ 'tenant-profile.max-transport-messages-required' | translate}} +
@@ -147,7 +155,7 @@
- + tenant-profile.max-j-s-executions {{ 'tenant-profile.max-j-s-executions-range' | translate}} + - + tenant-profile.max-transport-data-points {{ 'tenant-profile.max-transport-data-points-range' | translate}} +
- + tenant-profile.max-rule-node-executions-per-message {{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}} +
@@ -196,7 +207,7 @@ {{ 'tenant-profile.time-to-live' | translate }} tenant-profile.unlimited
- + tenant-profile.max-d-p-storage-days {{ 'tenant-profile.max-d-p-storage-days-range' | translate}} + - + tenant-profile.alarms-ttl-days {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}} +
- + tenant-profile.default-storage-ttl-days {{ 'tenant-profile.default-storage-ttl-days-range' | translate}} + - + tenant-profile.rpc-ttl-days {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}} +
@@ -256,7 +271,8 @@ {{ 'tenant-profile.sms-enabled' | translate }} - + tenant-profile.max-sms -
- +
+ tenant-profile.max-emails {{ 'tenant-profile.max-emails-range' | translate}} + - + tenant-profile.max-created-alarms {{ 'tenant-profile.max-created-alarms-range' | translate}} +
@@ -301,7 +319,7 @@ {{ 'tenant-profile.ota-files-in-bytes' | translate }} tenant-profile.unlimited
- + tenant-profile.maximum-resources-sum-data-size {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} + - + tenant-profile.maximum-ota-packages-sum-data-size - {{ 'tenant-profile.maximum-ota-packages-sum-data-size-required' | translate}} + {{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}} - {{ 'tenant-profile.maximum-ota-packages-sum-data-size-range' | translate}} + {{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}} +
diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html index 9fee72fef4..56b20d8aa8 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -37,7 +37,7 @@ -
+
@@ -75,7 +75,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html index 7014570235..f44ff6138c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html @@ -22,11 +22,14 @@
- - - + + {{ 'widget.resource-is-module' | translate }} @@ -40,16 +43,15 @@ close
-
- -
+
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..73ebfb37c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -0,0 +1,61 @@ +/// +/// 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'; +import { + EntitiesTableBasicConfigComponent +} from '@home/components/widget/config/basic/cards/entities-table-basic-config.component'; +import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; +import { DataKeyRowComponent } from '@home/components/widget/config/basic/common/data-key-row.component'; + +@NgModule({ + declarations: [ + WidgetActionsPanelComponent, + SimpleCardBasicConfigComponent, + EntitiesTableBasicConfigComponent, + DataKeyRowComponent, + DataKeysPanelComponent + ], + imports: [ + CommonModule, + SharedModule, + WidgetConfigComponentsModule + ], + exports: [ + WidgetActionsPanelComponent, + SimpleCardBasicConfigComponent, + EntitiesTableBasicConfigComponent, + DataKeyRowComponent, + DataKeysPanelComponent + ] +}) +export class BasicWidgetConfigModule { +} + +export const basicWidgetConfigComponentsMap: {[key: string]: Type} = { + 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent, + 'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html new file mode 100644 index 0000000000..ee76bdf472 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html @@ -0,0 +1,86 @@ + + + + + + + + +
+
widget-config.appearance
+
+ + {{ 'widget-config.card-title' | translate }} + + + + +
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + +
+
+
+
{{ 'widget-config.text-color' | translate }}
+
+ + + +
+
+
+
{{ 'widget-config.background' | translate }}
+
+ + + +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts new file mode 100644 index 0000000000..4ddd068bb4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts @@ -0,0 +1,154 @@ +/// +/// 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 { + DataKey, + Datasource, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation +} from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +@Component({ + selector: 'tb-entities-table-basic-config', + templateUrl: './entities-table-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponent { + + public get displayTimewindowConfig(): boolean { + const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + public get datasource(): Datasource { + const datasources: Datasource[] = this.entitiesTableWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + entitiesTableWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.entitiesTableWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [{ name: 'name', type: DataKeyType.entityField }]); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + this.entitiesTableWidgetConfigForm = this.fb.group({ + timewindowConfig: [{ + useDashboardTimewindow: configData.config.useDashboardTimewindow, + displayTimewindow: configData.config.useDashboardTimewindow, + timewindow: configData.config.timewindow + }, []], + datasources: [configData.config.datasources, []], + columns: [this.getColumns(configData.config.datasources), []], + showTitle: [configData.config.showTitle, []], + title: [configData.config.settings?.entitiesTitle, []], + showTitleIcon: [configData.config.showTitleIcon, []], + titleIcon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + 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.setColumns(config.columns, this.widgetConfig.config.datasources); + this.widgetConfig.config.actions = config.actions; + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.entitiesTitle = config.title; + this.widgetConfig.config.showTitleIcon = config.showTitleIcon; + this.widgetConfig.config.titleIcon = config.titleIcon; + this.widgetConfig.config.iconColor = config.iconColor; + this.widgetConfig.config.color = config.color; + this.widgetConfig.config.backgroundColor = config.backgroundColor; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showTitleIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.entitiesTableWidgetConfigForm.get('showTitle').value; + const showTitleIcon: boolean = this.entitiesTableWidgetConfigForm.get('showTitleIcon').value; + if (showTitle) { + this.entitiesTableWidgetConfigForm.get('title').enable(); + this.entitiesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); + if (showTitleIcon) { + this.entitiesTableWidgetConfigForm.get('titleIcon').enable(); + this.entitiesTableWidgetConfigForm.get('iconColor').enable(); + } else { + this.entitiesTableWidgetConfigForm.get('titleIcon').disable(); + this.entitiesTableWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.entitiesTableWidgetConfigForm.get('title').disable(); + this.entitiesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); + this.entitiesTableWidgetConfigForm.get('titleIcon').disable(); + this.entitiesTableWidgetConfigForm.get('iconColor').disable(); + } + this.entitiesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); + this.entitiesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); + } + + private getColumns(datasources?: Datasource[]): DataKey[] { + if (datasources && datasources.length) { + return datasources[0].dataKeys || []; + } + return []; + } + + private setColumns(columns: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + datasources[0].dataKeys = columns; + } + } + +} 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..b59170fb89 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts @@ -0,0 +1,117 @@ +/// +/// 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'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +@Component({ + selector: 'tb-simple-card-basic-config', + templateUrl: './simple-card-basic-config.component.html', + styleUrls: ['../basic-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, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.simpleCardWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]); + } + + 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/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html new file mode 100644 index 0000000000..0ae6ffb05e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -0,0 +1,170 @@ + +
+ + + +
+
+
+ + notifications + + + timeline + +
+
+ + + +
+
+ + +
+
+ +
+ + + + + notifications + + + timeline + + + + + +
+
+ entity.no-keys-found +
+ + + {{ translate.get('entity.no-key-matching', + {key: truncate.transform(keySearchText, true, 6, '...')}) | async }} + + + entity.create-new-key + + + {{'entity.create-new-key' | translate }} + notifications + + + timeline + + +
+
+
+
+ + + +
+ + +
+
+ + +
+
+ + + +
+
+ + + f() + + + + + + + + {{ modelValue?.aggregationType }}({{ modelValue?.name }}) + + + {{modelValue?.name}} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss new file mode 100644 index 0000000000..5b3d4f1a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss @@ -0,0 +1,53 @@ +/** + * 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-data-key-row { + height: 38px; + display: flex; + flex-direction: row; + gap: 12px; + padding-left: 12px; + + .mat-mdc-form-field.tb-inline-field.tb-key-field { + .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { + .mat-mdc-form-field-infix { + padding-top: 0; + padding-bottom: 6px; + .mdc-evolution-chip-set .mdc-evolution-chip { + margin: 0; + } + input.mat-mdc-chip-input { + height: 32px; + margin-left: 0; + } + } + } + .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + .tb-attribute-chip { + .tb-chip-labels { + background: transparent; + } + } + } + } + + .tb-color-field, .tb-units-field, .tb-decimals-field { + width: 60px; + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts new file mode 100644 index 0000000000..d434ef74e3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts @@ -0,0 +1,426 @@ +/// +/// 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, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; +import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AggregationType } from '@shared/models/time/time.models'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { Observable, of } from 'rxjs'; +import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone } from '@core/utils'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; + +export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => { + const dataKey: DataKey = control.value; + if (!dataKey || !dataKey.type || !dataKey.name) { + return { + dataKey: true + }; + } + return null; +}; + +@Component({ + selector: 'tb-data-key-row', + templateUrl: './data-key-row.component.html', + styleUrls: ['./data-key-row.component.scss', '../../data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { + + dataKeyTypes = DataKeyType; + widgetTypes = widgetType; + + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + @ViewChild('keyInput') keyInput: ElementRef; + @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; + @ViewChild('chipList') chipList: MatChipGrid; + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + @Input() + entityAliasId: string; + + @Input() + deviceId: string; + + keyFormControl: UntypedFormControl; + + keyRowFormGroup: UntypedFormGroup; + + modelValue: DataKey; + + filteredKeys: Observable>; + + keySearchText = ''; + + private latestKeySearchTextResult: Array = null; + private keyFetchObservable$: Observable> = null; + + get dataKeyType(): DataKeyType { + return this.dataKeysPanelComponent.dataKeyType; + } + + get alarmKeys(): Array { + return this.dataKeysPanelComponent.alarmKeys; + } + + get functionTypeKeys(): Array { + return this.dataKeysPanelComponent.functionTypeKeys; + } + + get hideDataKeyColor(): boolean { + return this.dataKeysPanelComponent.hideDataKeyColor; + } + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get widget(): Widget { + return this.widgetConfigComponent.widget; + } + + get dashboard(): Dashboard { + return this.widgetConfigComponent.dashboard; + } + + get aliasController(): IAliasController { + return this.widgetConfigComponent.aliasController; + } + + get datakeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + get dataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; + } + + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + + get displayUnitsOrDigits() { + return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type); + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + public translate: TranslateService, + public truncate: TruncatePipe, + private dataKeysPanelComponent: DataKeysPanelComponent, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keyFormControl = this.fb.control(''); + this.keyRowFormGroup = this.fb.group({ + label: [null, []], + color: [null, []], + units: [null, []], + decimals: [null, []], + }); + this.keyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.filteredKeys = this.keyFormControl.valueChanges + .pipe( + tap((value: string | DataKey) => { + if (value && typeof value !== 'string') { + this.addKeyFromChipValue(value); + } else if (value === null) { + this.clearKeyChip(this.keyInput.nativeElement.value); + } + }), + filter((value) => typeof value === 'string'), + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchKeys(name) ), + share() + ); + } + + private reset() { + if (this.keyInput) { + this.keyInput.nativeElement.value = ''; + } + this.keyFormControl.patchValue('', {emitEvent: false}); + this.latestKeySearchTextResult = null; + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['deviceId', 'entityAliasId'].includes(propName)) { + this.clearKeySearchCache(); + } else if (['datasourceType'].includes(propName)) { + if ([DatasourceType.device, DatasourceType.entity].includes(change.previousValue) && + [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) { + this.clearKeySearchCache(); + } else { + this.clearKeySearchCache(); + setTimeout(() => { + this.reset(); + }, 1); + } + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyRowFormGroup.disable({emitEvent: false}); + } else { + this.keyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey): void { + this.modelValue = value || {} as DataKey; + this.keyRowFormGroup.patchValue( + { + label: value?.label, + color: value?.color, + units: value?.units, + decimals: value?.decimals + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + dataKeyHasAggregation(): boolean { + return this.widgetConfigComponent.widgetType === widgetType.latest && this.modelValue?.type === DataKeyType.timeseries + && this.modelValue?.aggregationType && this.modelValue?.aggregationType !== AggregationType.NONE; + } + + dataKeyHasPostprocessing(): boolean { + return !!this.modelValue?.postFuncBody; + } + + displayKeyFn(key?: DataKey): string | undefined { + return key ? key.name : undefined; + } + + createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { + this.addKeyFromChipValue({name: name ? name.trim() : '', type: dataKeyType}); + } + + addKey(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim() && this.dataKeyType) { + this.addKeyFromChipValue({name: value.trim(), type: this.dataKeyType}); + } else { + this.clearKeyChip(); + } + } + + editKey() { + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(this.modelValue), + dataKeySettingsSchema: this.datakeySettingsSchema, + dataKeySettingsDirective: this.dataKeySettingsDirective, + dashboard: this.dashboard, + aliasController: this.aliasController, + widget: this.widget, + widgetType: this.widgetType, + deviceId: this.deviceId, + entityAliasId: this.entityAliasId, + showPostProcessing: this.widgetType !== widgetType.alarm, + callbacks: this.callbacks, + hideDataKeyLabel: false, + hideDataKeyColor: this.hideDataKeyColor, + hideDataKeyUnits: !this.displayUnitsOrDigits, + hideDataKeyDecimals: !this.displayUnitsOrDigits + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.modelValue = updatedDataKey; + this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); + this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false}); + this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); + this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); + this.updateModel(); + } + }); + } + + removeKey() { + this.modelValue = {} as DataKey; + this.updateModel(); + this.clearKeyChip(); + } + + textIsNotEmpty(text: string): boolean { + return text && text.length > 0; + } + + clearKeyChip(value: string = '', focus = true) { + this.autocomplete.closePanel(); + this.keyInput.nativeElement.value = value; + this.keyFormControl.patchValue(value, {emitEvent: focus}); + if (focus) { + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + } + + onKeyInputFocus() { + if (!this.modelValue.type) { + this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchTextResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createDataKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchTextResult = res) + ); + } + return of(this.latestKeySearchTextResult); + } + + private getKeys(): Observable> { + if (this.keyFetchObservable$ === null) { + let fetchObservable: Observable>; + 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 || + this.datasourceType === DatasourceType.device && this.deviceId) { + const dataKeyTypes = [DataKeyType.timeseries]; + if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.attribute); + dataKeyTypes.push(DataKeyType.entityField); + if (this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.alarm); + } + } + if (this.datasourceType === DatasourceType.device) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } + } else { + fetchObservable = of([]); + } + this.keyFetchObservable$ = fetchObservable.pipe( + publishReplay(1), + refCount() + ); + } + return this.keyFetchObservable$; + } + + private createDataKeyFilter(query: string): (key: DataKey) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.name.toLowerCase().startsWith(lowercaseQuery); + } + + private addKeyFromChipValue(chip: DataKey) { + this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); + if (!this.keyRowFormGroup.get('label').value) { + this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); + } + this.updateModel(); + this.clearKeyChip('', false); + } + + private clearKeySearchCache() { + this.keySearchText = ''; + this.keyFetchObservable$ = null; + this.latestKeySearchTextResult = null; + } + + private updateModel() { + const value: DataKey = this.keyRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html new file mode 100644 index 0000000000..754f0052c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -0,0 +1,67 @@ + +
+
{{ panelTitle }}
+
+
+
datakey.key
+
datakey.label
+
datakey.color
+
widget-config.units-short
+
widget-config.decimals-short
+
+
+
+
+ + +
+ + +
+
+
+
+
+ +
+
+ + {{ noKeysText }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss new file mode 100644 index 0000000000..df8b187535 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss @@ -0,0 +1,76 @@ +/** + * 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-data-keys-table { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: 12px; + .tb-data-keys-header { + height: 48px; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 12px; + padding-left: 12px; + .tb-data-keys-header-cell { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + &.tb-color-header, &.tb-units-header, &.tb-decimals-header { + width: 60px; + } + &.tb-actions-header { + width: 76px; + } + } + } + .tb-data-keys-body { + display: flex; + flex-direction: column; + gap: 12px; + } + .tb-prompt { + height: 38px; + } +} + +.tb-data-keys-table-row { + height: 38px; + display: flex; + flex-direction: row; + gap: 12px; + background: #fff; + + .tb-data-keys-table-row-buttons { + display: flex; + flex-direction: row; + button.mat-mdc-icon-button.mat-mdc-button-base { + padding: 7px; + width: 38px; + height: 38px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts new file mode 100644 index 0000000000..3d8b80c1df --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts @@ -0,0 +1,237 @@ +/// +/// 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, + OnChanges, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; +import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { alarmFields } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-data-keys-panel', + templateUrl: './data-keys-panel.component.html', + styleUrls: ['./data-keys-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges, Validator { + + @Input() + disabled: boolean; + + @Input() + panelTitle: string; + + @Input() + addKeyTitle: string; + + @Input() + removeKeyTitle: string; + + @Input() + noKeysText: string; + + @Input() + datasourceType: DatasourceType; + + @Input() + entityAliasId: string; + + @Input() + deviceId: string; + + @Input() + @coerceBoolean() + hideDataKeyColor = false; + + dataKeyType: DataKeyType; + alarmKeys: Array; + functionTypeKeys: Array; + + keysListFormGroup: UntypedFormGroup; + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get datakeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + private utils: UtilsService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [this.fb.array([]), []] + }); + this.keysListFormGroup.valueChanges.subscribe( + (val) => this.propagateChange(this.keysListFormGroup.get('keys').value) + ); + this.alarmKeys = []; + for (const name of Object.keys(alarmFields)) { + this.alarmKeys.push({ + name, + type: DataKeyType.alarm + }); + } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } + this.updateParams(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['datasourceType'].includes(propName)) { + this.updateParams(); + } + } + } + } + + private updateParams() { + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + } else { + if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) { + this.dataKeyType = DataKeyType.timeseries; + } else { + this.dataKeyType = null; + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey[] | undefined): void { + this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + return this.keysListFormGroup.valid ? null : { + dataKeyRows: { + valid: false, + }, + }; + } + + keyDrop(event: CdkDragDrop) { + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const key = keysArray.at(event.previousIndex); + keysArray.removeAt(event.previousIndex); + keysArray.insert(event.currentIndex, key); + } + + keysFormArray(): UntypedFormArray { + return this.keysListFormGroup.get('keys') as UntypedFormArray; + } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + (this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index); + } + + addKey() { + const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema); + dataKey.label = ''; + dataKey.decimals = 0; + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]); + keysArray.push(keyControl); + } + + private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { + const keysControls: Array = []; + if (keys) { + keys.forEach((key) => { + keysControls.push(this.fb.control(key, [dataKeyRowValidator])); + }); + } + return this.fb.array(keysControls); + } + +} 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..469ff191ed --- /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: [], + 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 93% 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..a7cf923c44 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; @@ -135,6 +155,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con private dataKeySettingsData: JsonFormComponentData; private alarmKeys: Array; + private functionTypeKeys: Array; filteredKeys: Observable>; private latestKeySearchResult: Array = null; @@ -163,6 +184,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con type: DataKeyType.alarm }); } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { this.displayAdvanced = true; @@ -376,10 +404,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con let fetchObservable: Observable>; if (this.modelValue.type === DataKeyType.alarm) { fetchObservable = of(this.alarmKeys); + } else if (this.modelValue.type === DataKeyType.function) { + fetchObservable = of(this.functionTypeKeys); } 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/config/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss new file mode 100644 index 0000000000..7d01f41cb5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss @@ -0,0 +1,124 @@ +/** + * 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-datakeys-container { + display: flex; + flex-wrap: wrap; + width: 100%; + + input.tb-dragging { + display: none; + } +} + +.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + overflow: hidden; + 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 { + overflow: hidden; + } + } + .tb-attribute-chip { + max-width: 100%; + color: rgb(66, 66, 66); + .tb-chip-drag-handle { + padding: 3px; + height: 24px; + cursor: move; + mat-icon { + pointer-events: none; + } + } + .tb-chip-labels { + display: flex; + 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; + .mat-icon.tb-datakey-icon { + margin-right: 4px; + margin-left: 4px; + } + .tb-agg-func { + font-style: italic; + color: #0c959c; + } + } + .tb-chip-separator { + white-space: pre; + } + } + .mat-mdc-chip-remove.mat-mdc-icon-button { + color: inherit; + opacity: inherit; + } + } + + &.tb-datakey-chip-dnd-placeholder { + min-width: 120px; + border: 2px dashed rgba(0, 0, 0, 0.2); + } + &.tb-chip-dragging { + display: none; + } + .tb-dragging-chip-image-fill { + background-color: rgba(0,0,0,0.3); + border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); + display: none; + pointer-events: none; + } + .tb-dragging-chip-image { + background-color: var(--mdc-chip-elevated-container-color, transparent); + border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); + overflow: hidden; + height: 32px; + line-height: 20px; + .tb-dragging-chip-image-fill { + display: block; + } + } +} + +.mat-icon.tb-datakey-icon { + vertical-align: middle; + & > svg { + vertical-align: initial; + } + &.new-key { + margin-left: 8px; + margin-right: 8px; + } +} 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..ab7411488e --- /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..b1f203baaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts @@ -0,0 +1,279 @@ +/// +/// 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; + } + + public get hideDataKeys(): boolean { + return this.datasourcesComponent?.hideDataKeys; + } + + @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..ae674a4d55 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts @@ -0,0 +1,333 @@ +/// +/// 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'], + 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() + @coerceBoolean() + hideDataKeys = 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..31436c9415 --- /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: [], + 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..9bda9345dc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -0,0 +1,161 @@ +/// +/// 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 { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; + +export interface IBasicWidgetConfigComponent { + isAdd: boolean; + 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 { + + isAdd = false; + + 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, + protected widgetConfigComponent: WidgetConfigComponent) { + super(store); + } + + ngOnInit() {} + + ngAfterViewInit(): void { + setTimeout(() => { + if (!this.validateConfig()) { + this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue())); + } + }, 0); + } + + protected setupConfig(widgetConfig: WidgetConfigComponentData) { + if (this.isAdd) { + this.setupDefaults(widgetConfig); + } + 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(() => { + this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue())); + }); + } + + protected setupDefaults(configData: WidgetConfigComponentData) {} + + 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 setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) { + let datasources = configData.config.datasources; + if (!datasources || !datasources.length) { + datasources = [ + { + type: DatasourceType.device, + dataKeys: [] + } + ]; + configData.config.datasources = datasources; + } + let dataKeys = datasources[0].dataKeys; + if (!dataKeys) { + dataKeys = []; + datasources[0].dataKeys = dataKeys; + } + if (keys && keys.length) { + dataKeys.length = 0; + keys.forEach(key => { + const dataKey = + this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); + if (key.label) { + dataKey.label = key.label; + } + if (key.units) { + dataKey.units = key.units; + } + if (isDefinedAndNotNull(key.decimals)) { + dataKey.decimals = key.decimals; + } + dataKeys.push(dataKey); + }); + } + } + + protected abstract configForm(): UntypedFormGroup; + + protected abstract onConfigSet(configData: WidgetConfigComponentData); + +} 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..a2ed480388 --- /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..e61511698d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts @@ -0,0 +1,68 @@ +/// +/// 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: [], + 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('', []); + this.unitsFormControl.valueChanges.subscribe(val => this.propagateChange(val)); + } + + 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/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss deleted file mode 100644 index a0b909af1a..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss +++ /dev/null @@ -1,120 +0,0 @@ -/** - * 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-datakeys-container { - display: flex; - flex-wrap: wrap; - width: 100%; - - input.tb-dragging { - display: none; - } - - .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { - overflow: hidden; - line-height: 20px; - height: 32px; - - .mat-mdc-chip-action { - overflow: hidden; - .mat-mdc-chip-action-label { - overflow: hidden; - } - } - .tb-attribute-chip { - max-width: 100%; - color: rgb(66, 66, 66); - font-weight: normal; - font-size: 16px; - .tb-chip-drag-handle { - cursor: move; - mat-icon { - pointer-events: none; - margin-right: 4px; - margin-left: 4px; - vertical-align: bottom; - } - } - .tb-chip-labels { - display: flex; - flex-direction: row; - align-items: center; - min-width: 0; - .tb-chip-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - .mat-icon.tb-datakey-icon { - margin-right: 4px; - margin-left: 4px; - } - .tb-agg-func { - font-style: italic; - color: #0c959c; - } - } - .tb-chip-separator { - white-space: pre; - } - } - .mat-mdc-chip-remove.mat-icon { - width: 24px; - min-width: 24px; - height: 24px; - font-size: 24px; - margin-right: 4px; - color: inherit; - opacity: inherit; - padding-left: 0; - } - } - - &.tb-datakey-chip-dnd-placeholder { - min-width: 120px; - border: 2px dashed rgba(0, 0, 0, 0.2); - } - &.tb-chip-dragging { - display: none; - } - .tb-dragging-chip-image-fill { - background-color: rgba(0,0,0,0.3); - border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); - display: none; - pointer-events: none; - } - .tb-dragging-chip-image { - background-color: var(--mdc-chip-elevated-container-color, transparent); - border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); - overflow: hidden; - height: 32px; - line-height: 20px; - .tb-dragging-chip-image-fill { - display: block; - } - } - } -} - -.mat-icon.tb-datakey-icon { - vertical-align: middle; - & > svg { - vertical-align: initial; - } - &.new-key { - margin-left: 8px; - margin-right: 8px; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts index d9ab65813f..dc3e9c7ac2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts @@ -33,6 +33,8 @@ import { CustomDialogComponent, CustomDialogData } from '@home/components/widget/dialog/custom-dialog.component'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; export interface CustomDialogContainerData { controller: (instance: CustomDialogComponent) => void; @@ -54,6 +56,8 @@ export class CustomDialogContainerComponent extends DialogComponent, + private dialogService: DialogService, + private translate: TranslateService, @Inject(MAT_DIALOG_DATA) public data: CustomDialogContainerData) { super(store, router, dialogRef); let customDialogData: CustomDialogData = { @@ -72,7 +76,19 @@ export class CustomDialogContainerComponent extends DialogComponent +
+ + +
+ + +
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/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html index e7ce8a1a02..2dbedf96bd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html @@ -66,4 +66,29 @@
+ + widgets.table.default-column-visibility + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} + + + + + widgets.table.column-selection-to-display + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts index 722df5bc5f..66a2c8b483 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts @@ -43,7 +43,9 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent useCellStyleFunction: false, cellStyleFunction: '', useCellContentFunction: false, - cellContentFunction: '' + cellContentFunction: '', + defaultColumnVisibility: 'visible', + columnSelectionToDisplay: 'enabled' }; } @@ -53,6 +55,8 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], useCellContentFunction: [settings.useCellContentFunction, []], cellContentFunction: [settings.cellContentFunction, [Validators.required]], + defaultColumnVisibility: [settings.defaultColumnVisibility, []], + columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html index a5b4f6bef2..8fb1629b13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html @@ -73,4 +73,29 @@ + + widgets.table.default-column-visibility + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} + + + + + widgets.table.column-selection-to-display + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts index 70fac42f98..3fe1f1027e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts @@ -44,7 +44,9 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom useCellStyleFunction: false, cellStyleFunction: '', useCellContentFunction: false, - cellContentFunction: '' + cellContentFunction: '', + defaultColumnVisibility: 'visible', + columnSelectionToDisplay: 'enabled' }; } @@ -56,6 +58,8 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], useCellContentFunction: [settings.useCellContentFunction, []], cellContentFunction: [settings.cellContentFunction, [Validators.required]], + defaultColumnVisibility: [settings.defaultColumnVisibility, []], + columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index 075cb5583c..ed4428fe45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -23,6 +23,9 @@ {{ 'widgets.table.enable-search' | translate }} + + {{ 'widgets.table.enable-select-column-display' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index 273dfbd703..0a57402870 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -41,6 +41,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon protected defaultSettings(): WidgetSettings { return { enableSearch: true, + enableSelectColumnDisplay: true, enableStickyHeader: true, enableStickyAction: true, reserveSpaceForHiddenAction: 'true', @@ -59,6 +60,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.timeseriesTableWidgetSettingsForm = this.fb.group({ enableSearch: [settings.enableSearch, []], + enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []], enableStickyHeader: [settings.enableStickyHeader, []], enableStickyAction: [settings.enableStickyAction, []], reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], 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 4c83594e44..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 @@ -290,7 +309,7 @@
widgets.chart.custom-legend-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/common/value-source.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts index d3858b89f6..6417ae4bcb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts @@ -215,7 +215,7 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr mergeMap((aliasInfo) => { return this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts index 2ada7e6cfb..22f8bf8e75 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts @@ -15,7 +15,13 @@ /// import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,6 +33,7 @@ import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/op import { DataKey } from '@shared/models/widget.models'; import { EntityService } from '@core/http/entity.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-device-key-autocomplete', @@ -199,7 +206,7 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI mergeMap((aliasInfo) => { return this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [EntityType.DEVICE], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts index 7e96d6ba0a..e79735e70d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts @@ -236,7 +236,7 @@ export class ImageMapProviderSettingsComponent extends PageComponent implements mergeMap((aliasInfo) => { return this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) 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/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 3cd343ea57..2a29776e7e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -31,6 +31,7 @@ type ColumnSelectionOptions = 'enabled' | 'disabled'; export interface TableWidgetSettings { enableSearch: boolean; + enableSelectColumnDisplay: boolean; enableStickyAction: boolean; enableStickyHeader: boolean; displayPagination: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 9aae6523ed..9342a2dc4c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -19,9 +19,12 @@ import { ChangeDetectorRef, Component, ElementRef, + Injector, Input, + OnDestroy, OnInit, QueryList, + StaticProvider, ViewChild, ViewChildren, ViewContainerRef @@ -64,8 +67,9 @@ import { CellStyleInfo, checkHasActions, constructTableCssString, + DisplayColumn, getCellContentInfo, - getCellStyleInfo, + getCellStyleInfo, getColumnDefaultVisibility, getColumnSelectionAvailability, getRowStyleInfo, getTableCellButtonActions, noDataMessage, @@ -75,12 +79,17 @@ import { TableWidgetDataKeySettings, TableWidgetSettings } from '@home/components/widget/lib/table-widget.models'; -import { Overlay } from '@angular/cdk/overlay'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; +import { + DISPLAY_COLUMNS_PANEL_DATA, + DisplayColumnsPanelComponent +} from '@home/components/widget/lib/display-columns-panel.component'; +import { ComponentPortal } from '@angular/cdk/portal'; export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { showTimestamp: boolean; @@ -105,6 +114,8 @@ interface TimeseriesHeader { dataKey: DataKey; sortable: boolean; show: boolean; + columnDefaultVisibility?: boolean; + columnSelectionAvailability?: boolean; styleInfo: CellStyleInfo; contentInfo: CellContentInfo; order?: number; @@ -131,7 +142,7 @@ interface TimeseriesTableSource { templateUrl: './timeseries-table-widget.component.html', styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] }) -export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { +export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { @Input() ctx: WidgetContext; @@ -169,6 +180,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private useEntityLabel = false; private dateFormatFilter: string; + private displayedColumns: Array = []; + private rowStylesInfo: RowStyleInfo; private subscriptions: Subscription[] = []; @@ -184,6 +197,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } }; + private columnDisplayAction: WidgetAction = { + name: 'entity.columns-to-display', + show: true, + icon: 'view_column', + onAction: ($event) => { + this.editColumnsToDisplay($event); + } + }; + constructor(protected store: Store, private elementRef: ElementRef, private overlay: Overlay, @@ -275,11 +297,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } private initialize() { - this.ctx.widgetActions = [this.searchAction ]; + this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length; this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; + this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; @@ -365,9 +388,82 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.sources.push(source); } } + this.prepareDisplayedColumn(); + this.sources[this.sourceIndex].displayedColumns = + this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def); this.updateActiveEntityInfo(); } + private editColumnsToDisplay($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const source = this.sources[this.sourceIndex]; + + this.prepareDisplayedColumn(); + + const providers: StaticProvider[] = [ + { + provide: DISPLAY_COLUMNS_PANEL_DATA, + useValue: { + columns: this.displayedColumns[this.sourceIndex], + columnsUpdated: (newColumns) => { + source.displayedColumns = newColumns.filter(value => value.display).map(value => value.def); + this.clearCache(); + } + } + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, + this.viewContainerRef, injector)); + this.ctx.detectChanges(); + } + + private prepareDisplayedColumn() { + if (!this.displayedColumns[this.sourceIndex]) { + this.displayedColumns[this.sourceIndex] = this.sources[this.sourceIndex].displayedColumns.map(value => { + let title = ''; + const header = this.sources[this.sourceIndex].header.find(column => column.index.toString() === value); + if (value === '0') { + title = 'Timestamp'; + } else if (value === 'actions') { + title = 'Actions'; + } else { + title = header.dataKey.name; + } + return { + title, + def: value, + display: header?.columnDefaultVisibility ?? true, + selectable: header?.columnSelectionAvailability ?? true + }; + }); + } + } + private prepareHeader(datasource: Datasource): TimeseriesHeader[] { const dataKeys = datasource.dataKeys; const latestDataKeys = datasource.latestDataKeys; @@ -377,6 +473,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const keySettings: TableWidgetDataKeySettings = dataKey.settings; const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); + const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); + const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); contentInfo.units = dataKey.units; contentInfo.decimals = dataKey.decimals; header.push({ @@ -386,6 +484,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI styleInfo, contentInfo, show: true, + columnDefaultVisibility, + columnSelectionAvailability, order: index + 2 }); }); @@ -396,6 +496,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings; const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); + const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); + const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); contentInfo.units = dataKey.units; contentInfo.decimals = dataKey.decimals; header.push({ @@ -405,13 +507,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI styleInfo, contentInfo, show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true, + columnDefaultVisibility, + columnSelectionAvailability, order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2) }); }); } - header = header.sort((a, b) => { - return a.order - b.order; - }); + header = header.sort((a, b) => a.order - b.order); return header; } 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..999e7e55ff 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 { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; +import Timeout = NodeJS.Timeout; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -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,80 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @Input() functionsOnly: boolean; + @Input() + @coerceBoolean() + hideHeader = false; + + @Input() + @coerceBoolean() + hideToggleHeader = false; + + @Input() + @coerceBoolean() + isAdd = 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 +230,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 +247,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 +278,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 +303,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 +317,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 +376,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 +402,200 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont writeValue(value: WidgetConfigComponentData): void { this.modelValue = value; - this.removeChangeSubscriptions(); + this.widgetType = this.modelValue?.widgetType; + this.setupConfig(this.isAdd); + } + + private setupConfig(isAdd = false) { 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(isAdd); + } 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(isAdd = false) { + 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.isAdd = isAdd; + 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 +618,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 +644,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 +665,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 +674,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 +706,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 +827,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, [EntityType.DEVICE], + {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, + dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) - ); - }), + )), catchError(() => of([] as Array)) ); } @@ -841,9 +867,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 +893,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 +909,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 0f8f6a9997..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,26 +15,14 @@ limitations under the License. --> -
- -
- -
- Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}} +
{{ 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/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 02a4bf5867..533fd0fea1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; import { QueueComponent } from '@home/pages/admin/queue/queue.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; @@ -44,6 +45,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut OAuth2SettingsComponent, HomeSettingsComponent, ResourcesLibraryComponent, + ResourcesTableHeaderComponent, QueueComponent, RepositoryAdminSettingsComponent, AutoCommitAdminSettingsComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html index 4524880098..2762e0d43a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html @@ -37,111 +37,306 @@ {{ 'admin.mail-from-required' | translate }} + - admin.smtp-protocol - - - {{protocol.toUpperCase()}} - - - -
- - admin.smtp-host - - - {{ 'admin.smtp-host-required' | translate }} - - - - admin.smtp-port - - {{smtpPortInput.value?.length || 0}}/5 - - {{ 'admin.smtp-port-required' | translate }} - - - {{ 'admin.smtp-port-invalid' | translate }} - - -
- - admin.timeout-msec - - {{timeoutInput.value?.length || 0}}/6 - - {{ 'admin.timeout-required' | translate }} - - - {{ 'admin.timeout-invalid' | translate }} - - - - {{ 'admin.enable-tls' | translate }} - - - admin.tls-version - - - {{ tlsVersion }} + admin.oauth2.smtp-provider + + + {{ templates.get(provider)?.name || 'Custom' }} - - {{ 'admin.enable-proxy' | translate }} - -
-
- - admin.proxy-host - - - {{ 'admin.proxy-host-required' | translate }} - + + + + + admin.connection-settings + + + + + admin.smtp-protocol + + + {{protocol.toUpperCase()}} + + - - admin.proxy-port - - - {{ 'admin.proxy-port-required' | translate }} +
+ + admin.smtp-host + + + {{ 'admin.smtp-host-required' | translate }} + + + + admin.smtp-port + + {{smtpPortInput.value?.length || 0}}/5 + + {{ 'admin.smtp-port-required' | translate }} + + + {{ 'admin.smtp-port-invalid' | translate }} + + +
+ + admin.timeout-msec + + {{timeoutInput.value?.length || 0}}/6 + + {{ 'admin.timeout-required' | translate }} - - {{ 'admin.proxy-port-range' | translate }} + + {{ 'admin.timeout-invalid' | translate }} -
- - admin.proxy-user - - + + {{ 'admin.enable-tls' | translate }} + + + admin.tls-version + + + {{ tlsVersion }} + + + + + {{ 'admin.enable-proxy' | translate }} + +
+
+ + admin.proxy-host + + + {{ 'admin.proxy-host-required' | translate }} + + + + admin.proxy-port + + + {{ 'admin.proxy-port-required' | translate }} + + + {{ 'admin.proxy-port-range' | translate }} + + +
+
+ + admin.proxy-user + + + + admin.proxy-password + + + +
+
+ + + +
+ admin.oauth2.authentication - admin.proxy-password - - + common.username + -
- - common.username - - - - {{ 'admin.change-password' | translate }} - - - common.password - - - +
+ + {{ 'admin.oauth2.basic' | translate }} + {{ 'admin.oauth2.oauth2' | translate }} + +
+
+
+
+
+ + {{ 'admin.change-password' | translate }} + + + common.password + + + +
+
+
+ + admin.oauth2.client-id + + + {{ 'admin.oauth2.client-id-required' | translate }} + + + {{ 'admin.oauth2.client-id-max-length' | translate }} + + + + + admin.oauth2.client-secret + + + {{ 'admin.oauth2.client-secret-required' | translate }} + + + {{ 'admin.oauth2.client-secret-max-length' | translate }} + + +
+ + + admin.oauth2.microsoft-tenant-id + + + {{ 'admin.oauth2.microsoft-tenant-id-required' | translate }} + + + + + + tenant-profile.advanced-settings + + + +
+ + admin.oauth2.authorization-uri + + + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + + + + admin.oauth2.token-uri + + + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + +
+ + admin.oauth2.scope + + + {{scope}} + cancel + + + + + {{ 'admin.oauth2.scope-required' | translate }} + + +
+
+ +
+ admin.oauth2.redirect-uri +
+
+
+ + admin.oauth2.protocol + + + {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} + + + + + admin.domain-name + + + {{ 'admin.error-verification-url' | translate }} + + + {{ 'admin.domain-name-max-length' | translate }} + + +
+ + {{ 'admin.domain-name-unique' | translate }} + +
+ +
+ + admin.oauth2.redirect-uri-template + + + + +
+
+
+
+
+
+ admin.oauth2.access-token-status + + {{ accessTokenStatus }} + +
+ +
+
-
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss index 66df772d2d..cd6c18af9e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss @@ -13,6 +13,95 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../../theme"; + :host { + .fields-group { + padding: 0 8px 8px; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + legend { + margin-bottom: 8px; + color: rgba(0, 0, 0, .7); + width: fit-content; + } + } + + .token-status { + font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif; + color: rgba(0,0,0, 0.6); + letter-spacing: 0.25px; + padding: 8px 0; + } + + ::ng-deep{ + .mat-expansion-panel { + .mat-expansion-panel-header { + height: 48px; + padding: 0 12px; + &.mat-expanded { + height: 48px; + } + } + .mat-expansion-panel-body { + padding: 0 12px; + } + &.configuration-panel { + border: 1px solid rgba(0, 0, 0, 0.2); + } + } + .mat-button-toggle-group.tb-notification-unread-toggle-group { + &.mat-button-toggle-group-appearance-standard { + border: none; + border-radius: 14px; + + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + } + + .mat-button-toggle { + background: rgba(0, 0, 0, 0.06); + height: 28px; + align-items: center; + display: flex; + + .mat-button-toggle-ripple { + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 12px; + } + } + + .mat-button-toggle-button { + color: #959595; + } + + .mat-button-toggle-focus-overlay { + border-radius: 14px; + margin: 2px; + } + + .mat-button-toggle-checked .mat-button-toggle-button { + background-color: $tb-primary-color; + color: #fff; + border-radius: 14px; + margin-left: 2px; + margin-right: 2px; + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 24px; + font-size: 16px; + font-weight: 500; + } + .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: .01; + } + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts index 7833c745f4..6815c81e31 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts @@ -14,20 +14,32 @@ /// limitations under the License. /// -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; -import { Router } from '@angular/router'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { AdminSettings, MailServerSettings, smtpPortPattern } from '@shared/models/settings.models'; +import { FormBuilder, FormGroup, UntypedFormArray, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + AdminSettings, + MailConfigTemplate, + MailServerOauth2Provider, + MailServerSettings, + smtpPortPattern, + SmtpProtocol +} from '@shared/models/settings.models'; import { AdminService } from '@core/http/admin.service'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; -import { isDefinedAndNotNull, isString } from '@core/utils'; -import { Subject } from 'rxjs'; +import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; +import { forkJoin, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { DomainSchema, domainSchemaTranslations, } from '@shared/models/oauth2.models'; +import { WINDOW } from '@core/services/window.service'; +import { AuthService } from '@core/auth/auth.service'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { MatChipInputEvent } from '@angular/material/chips'; @Component({ selector: 'tb-mail-server', @@ -35,40 +47,138 @@ import { takeUntil } from 'rxjs/operators'; styleUrls: ['./mail-server.component.scss', './settings-card.scss'] }) export class MailServerComponent extends PageComponent implements OnInit, OnDestroy, HasConfirmForm { - - mailSettings: UntypedFormGroup; adminSettings: AdminSettings; - smtpProtocols = ['smtp', 'smtps']; + smtpProtocols = Object.values(SmtpProtocol); showChangePassword = false; + protocols = Object.values(DomainSchema).filter(value => value !== DomainSchema.MIXED); + domainSchemaTranslations = domainSchemaTranslations; + + mailServerOauth2Provider = MailServerOauth2Provider; + tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; + helpLink: string; + + templates = new Map(); + + templateProvider = ['CUSTOM']; + + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + private destroy$ = new Subject(); + private DOMAIN_AND_PORT_REGEXP = /^(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?$/; + private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; + private loginProcessingUrl: string; + + mailSettings = this.fb.group({ + mailFrom: ['', [Validators.required]], + smtpProtocol: [SmtpProtocol.SMTP], + smtpHost: ['localhost', [Validators.required]], + smtpPort: [25, [Validators.required, + Validators.pattern(smtpPortPattern), + Validators.maxLength(5)]], + timeout: [10000, [Validators.required, + Validators.pattern(/^[0-9]{1,6}$/), + Validators.maxLength(6)]], + enableTls: [false], + tlsVersion: [{ value: null, disabled: true }], + enableProxy: [false], + proxyHost: [{ value: '', disabled: true }, [Validators.required]], + proxyPort: [{ value: null, disabled: true }, [Validators.required, Validators.min(1), Validators.max(65535)]], + proxyUser: [{ value: '', disabled: true }], + proxyPassword: [{ value: '', disabled: true }], + username: [''], + changePassword: [false], + password: [''], + enableOauth2: [false], + providerId: ['CUSTOM', [Validators.required]], + clientId: [{ value:'', disabled: true }, [Validators.required, Validators.maxLength(255)]], + clientSecret: [{ value:'', disabled: true }, [Validators.required, Validators.maxLength(2048)]], + providerTenantId: [{value: '', disabled: true}, [Validators.required]], + authUri: [{value: '', disabled: true}, [Validators.required, Validators.pattern(this.URL_REGEXP)]], + tokenUri: [{value: '', disabled: true}, [Validators.required, Validators.pattern(this.URL_REGEXP)]], + scope: [], + redirectUri: [{ value:'', disabled: true}] + }); + + private defaultConfiguration = { + providerId: 'CUSTOM', + smtpProtocol: SmtpProtocol.SMTP, + smtpHost: '', + smtpPort: null, + timeout: null, + enableTls: false, + tlsVersion: null, + enableProxy: false, + proxyHost: '', + proxyPort: null, + proxyUser: '', + proxyPassword: '', + enableOauth2: false, + clientId: '', + clientSecret: '', + providerTenantId: '', + authUri: '', + tokenUri: '', + scope: [], + redirectUri: '' + }; + + domainForm = this.fb.group({ + name: [this.window.location.hostname, [ + Validators.required, Validators.maxLength(255), + Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)] + ], + scheme: [DomainSchema.HTTPS, Validators.required] + }); constructor(protected store: Store, private router: Router, + private route: ActivatedRoute, private adminService: AdminService, + private authService: AuthService, private translate: TranslateService, - public fb: UntypedFormBuilder) { + public fb: FormBuilder, + @Inject(WINDOW) private window: Window) { super(store); } ngOnInit() { - this.buildMailServerSettingsForm(); - this.adminService.getAdminSettings('mail').subscribe( - (adminSettings) => { - this.adminSettings = adminSettings; - if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { - this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; - } - this.showChangePassword = - isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword) ? this.adminSettings.jsonValue.showChangePassword : true ; - delete this.adminSettings.jsonValue.showChangePassword; - this.mailSettings.reset(this.adminSettings.jsonValue); - this.enableMailPassword(!this.showChangePassword); - this.enableProxyChanged(); + this.mailServerSettingsForm(); + this.domainFormConfiguration(); + + forkJoin([ + this.adminService.getLoginProcessingUrl(), + this.adminService.getMailConfigTemplate(), + this.adminService.getAdminSettings('mail') + ]).subscribe(([loginProcessingUrl, mailConfigTemplate, adminSettings]) => { + this.loginProcessingUrl = loginProcessingUrl; + this.initTemplates(mailConfigTemplate); + this.adminSettings = adminSettings; + if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { + this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; } - ); + this.showChangePassword = isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword) + ? this.adminSettings.jsonValue.showChangePassword : true; + delete this.adminSettings.jsonValue.showChangePassword; + if (!this.adminSettings.jsonValue.providerId) { + this.adminSettings.jsonValue.providerId = 'CUSTOM'; + } + this.mailSettings.reset(this.adminSettings.jsonValue, {emitEvent: false}); + this.enableMailPassword(!this.showChangePassword); + this.enableProxyChanged(); + this.enableTls(this.adminSettings.jsonValue.enableTls); + this.helpLink = this.templates.get(this.adminSettings.jsonValue.providerId)?.helpLink || null; + if (this.adminSettings.jsonValue.enableOauth2) { + this.enableOauth2(!!this.adminSettings.jsonValue.enableOauth2); + this.enableProviderTenantIdChanged(this.adminSettings.jsonValue.providerId); + this.parseUrl(this.adminSettings.jsonValue.redirectUri); + this.mailSettings.get('redirectUri').patchValue(this.adminSettings.jsonValue.redirectUri, {emitEvent: false}); + } else { + this.mailSettings.get('enableOauth2').patchValue(false, {emitEvent: false}); + } + }); } ngOnDestroy() { @@ -77,56 +187,153 @@ export class MailServerComponent extends PageComponent implements OnInit, OnDest super.ngOnDestroy(); } - buildMailServerSettingsForm() { - this.mailSettings = this.fb.group({ - mailFrom: ['', [Validators.required]], - smtpProtocol: ['smtp'], - smtpHost: ['localhost', [Validators.required]], - smtpPort: ['25', [Validators.required, - Validators.pattern(smtpPortPattern), - Validators.maxLength(5)]], - timeout: ['10000', [Validators.required, - Validators.pattern(/^[0-9]{1,6}$/), - Validators.maxLength(6)]], - enableTls: [false], - tlsVersion: [], - enableProxy: [false, []], - proxyHost: ['', [Validators.required]], - proxyPort: ['', [Validators.required, Validators.min(1), Validators.max(65535)]], - proxyUser: [''], - proxyPassword: [''], - username: [''], - changePassword: [false], - password: [''] + private initTemplates(templates): void { + templates.map(provider => { + delete provider.additionalInfo; + this.templates.set(provider.providerId, provider); }); + this.templateProvider.push(...Array.from(this.templates.keys())); + this.templateProvider.sort(); + } + + private mailServerSettingsForm(): void { this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy')); this.registerDisableOnLoadFormControl(this.mailSettings.get('changePassword')); + + this.mailSettings.get('enableTls').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => this.enableTls(value)); + this.mailSettings.get('enableProxy').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { this.enableProxyChanged(); }); + this.mailSettings.get('changePassword').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value) => { this.enableMailPassword(value); }); + + this.mailSettings.get('enableOauth2').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe( value => { + this.enableOauth2(value); + this.enableProviderTenantIdChanged(this.mailSettings.get('providerId').value); + if (value && !this.mailSettings.get('redirectUri').value) { + this.mailSettings.get('redirectUri').patchValue(this.redirectURI(), {emitEvent: false}); + } + }); + + this.mailSettings.get('providerTenantId').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(tenantId => { + const authorizationUri = this.templates.get(this.mailServerOauth2Provider.OFFICE_365).authorizationUri.replace('%s', `${tenantId}`); + const accessTokenUri = this.templates.get(this.mailServerOauth2Provider.OFFICE_365).accessTokenUri.replace('%s', `${tenantId}`); + this.mailSettings.get('authUri').patchValue(authorizationUri, {emitEvent: false}); + this.mailSettings.get('tokenUri').patchValue(accessTokenUri, {emitEvent: false}); + }); + + this.mailSettings.get('providerId').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe( value => { + if (value === this.mailServerOauth2Provider.CUSTOM || !value) { + this.mailSettings.reset({...this.adminSettings.jsonValue, ...this.defaultConfiguration}, {emitEvent: false}); + } else { + const config = this.templates.get(value); + this.helpLink = config.helpLink; + this.mailSettings.patchValue({ + smtpProtocol: SmtpProtocol[config.smtpProtocol], + smtpHost: config.smtpHost, + smtpPort: config.smtpPort, + timeout: config.timeout, + enableTls: config.enableTls, + tlsVersion: config.tlsVersion, + authUri: config.authorizationUri, + tokenUri: config.accessTokenUri, + scope: config.scope, + enableOauth2: false, + enableProxy: false, + proxyHost: '', + proxyPort: null, + proxyUser: '', + proxyPassword: '', + clientId: '', + clientSecret: '', + providerTenantId: '', + redirectUri: '' + }, {emitEvent: false}); + } + this.enableTls(this.mailSettings.get('enableTls').value); + this.enableOauth2(this.mailSettings.get('enableOauth2').value); + this.enableProviderTenantIdChanged(value); + }); + } + + private domainFormConfiguration(): void { + this.domainForm.get('name').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe( + value => this.mailSettings.get('redirectUri').patchValue( + this.redirectURI(this.domainForm.get('scheme').value, value), + {emitEvent: false} + ) + ); + this.domainForm.get('scheme').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe( + (value) => this.mailSettings.get('redirectUri').patchValue(this.redirectURI(value), {emitEvent: false}) + ); } - enableProxyChanged(): void { + private enableOauth2(value: boolean): void { + if (value) { + this.mailSettings.get('clientId').enable({emitEvent: false}); + this.mailSettings.get('clientSecret').enable({emitEvent: false}); + this.mailSettings.get('redirectUri').enable({emitEvent: false}); + if (this.mailSettings.get('providerId').value === this.mailServerOauth2Provider.CUSTOM) { + this.mailSettings.get('authUri').enable({emitEvent: false}); + this.mailSettings.get('tokenUri').enable({emitEvent: false}); + } else { + this.mailSettings.get('authUri').disable({emitEvent: false}); + this.mailSettings.get('tokenUri').disable({emitEvent: false}); + } + } else { + this.mailSettings.get('clientId').disable({emitEvent: false}); + this.mailSettings.get('clientSecret').disable({emitEvent: false}); + this.mailSettings.get('redirectUri').disable({emitEvent: false}); + this.mailSettings.get('authUri').disable({emitEvent: false}); + this.mailSettings.get('tokenUri').disable({emitEvent: false}); + } + } + + private enableProviderTenantIdChanged(value: string): void { + if (value === this.mailServerOauth2Provider.OFFICE_365 && this.mailSettings.get('enableOauth2').value) { + this.mailSettings.get('providerTenantId').enable({emitEvent: false}); + } else { + this.mailSettings.get('providerTenantId').disable({emitEvent: false}); + } + } + + private enableProxyChanged(): void { const enableProxy: boolean = this.mailSettings.get('enableProxy').value; if (enableProxy) { - this.mailSettings.get('proxyHost').enable(); - this.mailSettings.get('proxyPort').enable(); + this.mailSettings.get('proxyHost').enable({emitEvent: false}); + this.mailSettings.get('proxyPort').enable({emitEvent: false}); + this.mailSettings.get('proxyUser').enable({emitEvent: false}); + this.mailSettings.get('proxyPassword').enable({emitEvent: false}); } else { - this.mailSettings.get('proxyHost').disable(); - this.mailSettings.get('proxyPort').disable(); + this.mailSettings.get('proxyHost').disable({emitEvent: false}); + this.mailSettings.get('proxyPort').disable({emitEvent: false}); + this.mailSettings.get('proxyUser').disable({emitEvent: false}); + this.mailSettings.get('proxyPassword').disable({emitEvent: false}); } } - enableMailPassword(enable: boolean) { + private enableMailPassword(enable: boolean) { if (enable) { this.mailSettings.get('password').enable({emitEvent: false}); } else { @@ -134,14 +341,21 @@ export class MailServerComponent extends PageComponent implements OnInit, OnDest } } + private enableTls(enable: boolean): void { + if (enable) { + this.mailSettings.get('tlsVersion').enable({emitEvent: false}); + } else { + this.mailSettings.get('tlsVersion').disable({emitEvent: false}); + } + } + sendTestMail(): void { this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue}; - this.adminService.sendTestMail(this.adminSettings).subscribe( - () => { - this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), - type: 'success' })); - } - ); + this.adminService.sendTestMail(this.adminSettings).subscribe({ + next: () => this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), + type: 'success' })), + error: error => this.store.dispatch(new ActionNotificationShow({message: error.error.message, type: 'error'})) + }); } save(): void { @@ -150,18 +364,88 @@ export class MailServerComponent extends PageComponent implements OnInit, OnDest (adminSettings) => { this.adminSettings = adminSettings; this.showChangePassword = true; - this.mailSettings.reset(this.adminSettings.jsonValue); + this.mailSettings.reset(this.adminSettings.jsonValue, {emitEvent: false}); + this.domainForm.reset(this.domainForm.value); + this.parseUrl(this.adminSettings.jsonValue.redirectUri); } ); } - confirmForm(): UntypedFormGroup { + generateAccessToken(): void { + this.adminService.generateAccessToken().subscribe( + uri => this.window.location.href = uri + ); + } + + redirectURI(schema?: DomainSchema, name?: string): string { + const domainInfo = this.domainForm.value; + if (domainInfo.name !== '') { + const protocol = isDefined(schema) ? schema.toLowerCase() : domainInfo.scheme.toLowerCase(); + const domainName = isDefined(name) ? name : domainInfo.name; + return `${protocol}://${domainName}${this.loginProcessingUrl}`; + } + return ''; + } + + private parseUrl(value: string): void { + if (value) { + const url = new URL(value); + this.domainForm.get('scheme').patchValue( + url.protocol.startsWith('https') ? DomainSchema.HTTPS : DomainSchema.HTTP, {emitEvent: false} + ); + this.domainForm.get('name').patchValue(url.host, {emitEvent: false}); + } + } + + get accessTokenButtonName(): string { + return this.translate.instant( + this.adminSettings.jsonValue.tokenGenerated ? 'admin.oauth2.update-access-token' : 'admin.oauth2.generate-access-token' + ); + } + + get accessTokenStatus(): string { + return this.translate.instant( + this.adminSettings.jsonValue.tokenGenerated ? 'admin.oauth2.token-status-generated' : 'admin.oauth2.token-status-not-generated' + ); + } + + confirmForm(): FormGroup { return this.mailSettings; } private get mailSettingsFormValue(): MailServerSettings { - const formValue = this.mailSettings.value; + const formValue = this.mailSettings.getRawValue() as Required; delete formValue.changePassword; return formValue; } + + trackByParams(index: number): number { + return index; + } + + removeScope(i: number): void { + const controller = this.mailSettings.get('scope') as UntypedFormArray; + controller.removeAt(i); + controller.markAsTouched(); + controller.markAsDirty(); + } + + addScope(event: MatChipInputEvent): void { + const input = event.chipInput.inputElement; + const value = event.value; + const controller = this.mailSettings.get('scope') as UntypedFormArray; + if ((value.trim() !== '')) { + controller.push(this.fb.control(value.trim())); + controller.markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + toggleEditMode(path: string): void { + this.mailSettings.get(path).disabled ? this.mailSettings.get(path).enable() : this.mailSettings.get(path).disable(); + } + } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index a488bd3807..733e5ef2b3 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -36,6 +36,7 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources- import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { map } from 'rxjs/operators'; +import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; @Injectable() export class ResourcesLibraryTableConfigResolver implements Resolve> { @@ -53,6 +54,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve resource ? resource.title : ''; @@ -61,11 +63,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve('createdTime', 'common.created-time', this.datePipe, '150px'), new EntityTableColumn('title', 'resource.title', '60%'), new EntityTableColumn('resourceType', 'resource.resource-type', '40%', - entity => this.resourceTypesTranslationMap.get(entity.resourceType)), + entity => this.translate.instant(this.resourceTypesTranslationMap.get(entity.resourceType))), new EntityTableColumn('tenantId', 'resource.system', '60px', - entity => { - return checkBoxCell(entity.tenantId.id === NULL_UUID); - }), + entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), ); this.config.cellActionDescriptors.push( @@ -83,7 +83,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve this.translate.instant('resource.delete-resources-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); - this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink); + this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType); this.config.loadEntity = id => this.resourceService.getResource(id.id); this.config.saveEntity = resource => this.saveResource(resource); this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); @@ -112,6 +112,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve { this.config.tableTitle = this.translate.instant('resource.resources-library'); + this.config.componentsData = { + resourceType: '' + }; const authUser = getCurrentAuthUser(this.store); this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); @@ -124,7 +127,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve {}); } downloadResource($event: Event, resource: ResourceInfo) { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html index c4208d1fcd..f955d26f0d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html @@ -52,7 +52,7 @@ resource.resource-type - {{ resourceTypesTranslationMap.get(resourceType) }} + {{ resourceTypesTranslationMap.get(resourceType) | translate }} @@ -66,7 +66,7 @@ {{ 'resource.title-max-length' | translate }} - -
+
resource.file-name diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts index 078f87b5ef..99d8697dc5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { EntityComponent } from '@home/components/entity/entity.component'; import { Resource, @@ -29,8 +29,9 @@ import { ResourceTypeMIMETypes, ResourceTypeTranslationMap } from '@shared/models/resource.models'; -import {filter, pairwise, startWith, takeUntil} from 'rxjs/operators'; +import { filter, startWith, takeUntil } from 'rxjs/operators'; import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-resources-library', @@ -39,7 +40,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions' export class ResourcesLibraryComponent extends EntityComponent implements OnInit, OnDestroy { readonly resourceType = ResourceType; - readonly resourceTypes = Object.values(this.resourceType); + readonly resourceTypes: ResourceType[] = Object.values(this.resourceType); readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; private destroy$ = new Subject(); @@ -48,7 +49,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme protected translate: TranslateService, @Inject('entity') protected entityValue: Resource, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, - public fb: UntypedFormBuilder, + public fb: FormBuilder, protected cd: ChangeDetectorRef) { super(store, fb, entityValue, entitiesTableConfigValue, cd); } @@ -56,7 +57,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme ngOnInit() { super.ngOnInit(); this.entityForm.get('resourceType').valueChanges.pipe( - startWith(ResourceType.LWM2M_MODEL), + startWith(ResourceType.JS_MODULE), filter(() => this.isAdd), takeUntil(this.destroy$) ).subscribe((type) => { @@ -87,33 +88,39 @@ export class ResourcesLibraryComponent extends EntityComponent impleme } } - buildForm(entity: Resource): UntypedFormGroup { - const form = this.fb.group( - { - title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], - resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, [Validators.required]], - fileName: [entity ? entity.fileName : null, [Validators.required]], - } - ); - if (this.isAdd) { - form.addControl('data', this.fb.control(null, Validators.required)); - } - return form; + buildForm(entity: Resource): FormGroup { + return this.fb.group({ + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], + resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required], + fileName: [entity ? entity.fileName : null, Validators.required], + data: [entity ? entity.data : null, Validators.required] + }); } updateForm(entity: Resource) { this.entity.name = entity.title; if (this.isEdit) { this.entityForm.get('resourceType').disable({emitEvent: false}); - this.entityForm.get('fileName').disable({emitEvent: false}); + if (entity.resourceType !== ResourceType.JS_MODULE) { + this.entityForm.get('fileName').disable({emitEvent: false}); + this.entityForm.get('data').disable({emitEvent: false}); + } } this.entityForm.patchValue({ resourceType: entity.resourceType, fileName: entity.fileName, - title: entity.title + title: entity.title, + data: entity.data }); } + prepareFormValue(formValue: Resource): Resource { + if (this.isEdit && !isDefinedAndNotNull(formValue.data)) { + delete formValue.data; + } + return super.prepareFormValue(formValue); + } + getAllowedExtensions() { try { return ResourceTypeExtension.get(this.entityForm.get('resourceType').value); diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html new file mode 100644 index 0000000000..ad8cf063e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html @@ -0,0 +1,30 @@ + + + resource.resource-type + + + {{ "resource.all-types" | translate }} + + + {{ resourceTypesTranslationMap.get(resourceType) | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts new file mode 100644 index 0000000000..dfe4bacff8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -0,0 +1,42 @@ +/// +/// 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 { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from '@shared/models/resource.models'; +import { PageLink } from '@shared/models/page/page-link'; + +@Component({ + selector: 'tb-resources-table-header', + templateUrl: './resources-table-header.component.html', + styleUrls: [] +}) +export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { + + readonly resourceTypes: ResourceType[] = Object.values(ResourceType); + readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; + + constructor(protected store: Store) { + super(store); + } + + resourceTypeChanged(resourceType: ResourceType) { + this.entitiesTableConfig.componentsData.resourceType = resourceType; + this.entitiesTableConfig.getTable().resetSortAndFilter(true); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index 130a1b2df0..ef29248767 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -164,6 +164,8 @@ admin.jwt.security-settings + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html index fc48392e8c..4e7a133009 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -59,8 +59,7 @@ diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 27b8968344..4ff13778e5 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -49,8 +49,7 @@
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index efb4851bd8..4cdaed942d 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -15,55 +15,48 @@ limitations under the License. --> -
+
-
- -
+ + + +
- + + - -
@@ -164,6 +157,14 @@ +
-
- -
+
@@ -270,6 +271,17 @@ [(ngModel)]="widget.latestDataKeySettingsDirective" (ngModelChange)="isDirty = true"/> + + {{ 'widget.has-basic-mode' | translate }} + + + widget.basic-mode-form-selector + +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss index 7d0d854d46..b8359b38db 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss @@ -31,18 +31,21 @@ tb-widget-editor { overflow-y: auto; } - mat-form-field.resource-field { - max-height: 40px; - margin: 10px 0 0; - .mat-mdc-text-field-wrapper { - padding-bottom: 0; - .mat-mdc-form-field-flex { - max-height: 40px; - .mat-mdc-form-field-infix { - border: 0; - padding-top: 7px; - padding-bottom: 7px; - min-height: 32px; + .resource-field { + mat-form-field { + .mat-mdc-text-field-wrapper { + padding-bottom: 0; + height: 40px; + + .mat-mdc-form-field-flex { + max-height: 40px; + + .mat-mdc-form-field-infix { + border: 0; + padding-top: 7px; + padding-bottom: 7px; + min-height: 32px; + } } } } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index b12a0dfa3a..0379c34620 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -489,9 +489,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe if (!this.gotError) { this.gotError = true; let errorInfo = 'Error:'; - if (details.name) { - errorInfo += ' ' + details.name + ':'; - } if (details.message) { errorInfo += ' ' + details.message; } 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/dialog/alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html index c3c4b59871..de56079efd 100644 --- a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html @@ -16,7 +16,7 @@ -->

{{data.title}}

-
+
diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html new file mode 100644 index 0000000000..8554a3d707 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html @@ -0,0 +1,30 @@ + +

{{title}}

+
+
{{ message }}
+
dialog.error-message-title
+
{{ errorMessage }}
+ + {{ 'dialog.error-details-title' | translate }} + + +
+
+ +
diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss new file mode 100644 index 0000000000..3ca391dbcd --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss @@ -0,0 +1,37 @@ +/** + * 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 { + .mat-mdc-dialog-content { + padding: 0 24px 24px; + } + .tb-error-alert-dialog-content { + .error-message-title { + font-style: italic; + } + .error-message-content { + color: red; + } + .error-details-content { + display: block; + border: solid 1px #d3d3d3; + padding: 8px; + border-radius: 4px; + } + & > *:not(:last-child) { + padding-bottom: 16px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts new file mode 100644 index 0000000000..8ef9032421 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.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 { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface ErrorAlertDialogData { + title: string; + message: string; + error: any; + ok: string; +} + +@Component({ + selector: 'tb-error-alert-dialog', + templateUrl: './error-alert-dialog.component.html', + styleUrls: ['./error-alert-dialog.component.scss'] +}) +export class ErrorAlertDialogComponent { + + title: string; + message: string; + errorMessage: string; + errorDetails?: string; + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ErrorAlertDialogData) { + this.title = this.data.title; + this.message = this.data.message; + this.errorMessage = this.data.error.message ? this.data.error.message : JSON.stringify(this.data.error); + if (this.data.error.stack) { + this.errorDetails = this.data.error.stack.replaceAll('\n', '
'); + } + } + +} 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/notification/template-autocomplete.component.html b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html index 8657228de6..3fe45a4da0 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html @@ -29,6 +29,14 @@ (click)="clear()"> close + + + + + + + {{ searchText }} + + + diff --git a/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts b/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts new file mode 100644 index 0000000000..8489fe0640 --- /dev/null +++ b/ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts @@ -0,0 +1,185 @@ +/// +/// 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, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { Observable, of } from 'rxjs'; +import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators'; +import { isDefinedAndNotNull, isEmptyStr, isEqual, isObject } from '@core/utils'; +import { ResourceInfo, ResourceType } from '@shared/models/resource.models'; +import { TbResourceId } from '@shared/models/id/tb-resource-id'; +import { ResourceService } from '@core/http/resource.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; + +@Component({ + selector: 'tb-resource-autocomplete', + templateUrl: './resource-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ResourceAutocompleteComponent), + multi: true + }] +}) +export class ResourceAutocompleteComponent implements ControlValueAccessor, OnInit { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + placeholder: string; + + @Input() + @coerceBoolean() + hideRequiredMarker = false; + + @Input() + @coerceBoolean() + allowAutocomplete = false; + + resourceFormGroup = this.fb.group({ + resource: [null] + }); + + filteredResources$: Observable>; + + searchText = ''; + + @ViewChild('resourceInput', {static: true}) resourceInput: ElementRef; + + private modelValue: string | TbResourceId; + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private resourceService: ResourceService) { + } + + ngOnInit(): void { + if(this.required) { + this.resourceFormGroup.get('resource').setValidators(Validators.required); + this.resourceFormGroup.get('resource').updateValueAndValidity({emitEvent: false}); + } + this.filteredResources$ = this.resourceFormGroup.get('resource').valueChanges + .pipe( + debounceTime(150), + tap(value => { + let modelValue; + if (isObject(value)) { + modelValue = value.id; + } else if (isEmptyStr(value)) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.title) : ''), + switchMap(name => this.fetchResources(name) ), + share() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean) { + this.disabled = isDisabled; + if (this.disabled) { + this.resourceFormGroup.disable({emitEvent: false}); + } else { + this.resourceFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | TbResourceId) { + if (isDefinedAndNotNull(value)) { + this.searchText = ''; + if (isObject(value) && typeof value !== 'string' && (value as TbResourceId).id) { + this.resourceService.getResourceInfo(value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe({ + next: resource => { + this.modelValue = resource.id; + this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false}); + }, + error: () => { + this.modelValue = ''; + this.resourceFormGroup.get('resource').patchValue(''); + } + }); + } else { + this.modelValue = value; + this.resourceFormGroup.get('resource').patchValue(value, {emitEvent: false}); + } + this.dirty = true; + } + } + + displayResourceFn(resource?: ResourceInfo | string): string { + return isObject(resource) ? (resource as ResourceInfo).title : resource as string; + } + + clear() { + this.resourceFormGroup.get('resource').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.resourceInput.nativeElement.blur(); + this.resourceInput.nativeElement.focus(); + }, 0); + } + + onFocus() { + if (this.dirty) { + this.resourceFormGroup.get('resource').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + private updateView(value: string | TbResourceId ) { + if (!isEqual(this.modelValue, value)) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + private fetchResources(searchText?: string): Observable> { + this.searchText = searchText; + return this.resourceService.getResources(new PageLink(50, 0, searchText), ResourceType.JS_MODULE, {ignoreLoading: true}).pipe( + catchError(() => of(null)), + map(data => data.data) + ); + } + +} diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html new file mode 100644 index 0000000000..2b9c227c30 --- /dev/null +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html @@ -0,0 +1,27 @@ + + + + {{ruleChain.name}} + + diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss new file mode 100644 index 0000000000..c538da5725 --- /dev/null +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.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 { + min-width: 52px; + width: 100%; + padding: 0 6px; + .tb-rule-chain-select { + display: flex; + height: 48px; + min-height: 100%; + pointer-events: all; + } +} diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts new file mode 100644 index 0000000000..003d85b751 --- /dev/null +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts @@ -0,0 +1,109 @@ +/// +/// 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 } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { map, share } from 'rxjs/operators'; +import { PageData } from '@shared/models/page/page-data'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { isDefinedAndNotNull } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { Direction } from '@shared/models/page/sort-order'; + +@Component({ + selector: 'tb-rule-chain-select', + templateUrl: './rule-chain-select.component.html', + styleUrls: ['./rule-chain-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleChainSelectComponent), + multi: true + }] +}) +export class RuleChainSelectComponent implements ControlValueAccessor, OnInit { + + @Input() + tooltipPosition: TooltipPosition = 'above'; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + ruleChainType: RuleChainType = RuleChainType.CORE; + + ruleChains$: Observable>; + + ruleChainId: string | null; + + private propagateChange = (v: any) => { }; + + constructor(private ruleChainService: RuleChainService) { + } + + ngOnInit() { + const pageLink = new PageLink(100, 0, null, { + property: 'name', + direction: Direction.ASC + }); + + this.ruleChains$ = this.getRuleChains(pageLink).pipe( + map((pageData) => pageData.data), + share() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: string | null): void { + if (isDefinedAndNotNull(value)) { + this.ruleChainId = value; + } + } + + ruleChainIdChanged() { + this.updateView(); + } + + private updateView() { + this.propagateChange(this.ruleChainId); + } + + private getRuleChains(pageLink: PageLink): Observable> { + return this.ruleChainService.getRuleChains(pageLink, this.ruleChainType, {ignoreLoading: true}); + } + +} diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.scss b/ui-ngx/src/app/shared/components/snack-bar-component.scss index 8832604510..b2db177e06 100644 --- a/ui-ngx/src/app/shared/components/snack-bar-component.scss +++ b/ui-ngx/src/app/shared/components/snack-bar-component.scss @@ -51,7 +51,7 @@ padding: 0 18px; margin: 8px; .toast-text { - padding: 0 6px; + padding: 8px; width: 100%; } button { diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.html b/ui-ngx/src/app/shared/components/time/timewindow.component.html index 91e5dd2f57..e445e99c98 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.html @@ -54,7 +54,7 @@ (click)="toggleTimewindow($event)" matTooltip="{{ 'timewindow.edit' | translate }}" [matTooltipPosition]="tooltipPosition"> - {{innerValue?.displayValue}} | {{innerValue.displayTimezoneAbbr}} + {{innerValue?.displayValue}} | {{innerValue?.displayTimezoneAbbr}}