Commit c78823332b5d016095557a7248c7ea96794f8cd5

Authored by Volodymyr Babak
2 parents ba1aa7a5 b187d762

Merge branch 'master' of github.com:thingsboard/thingsboard into edge/refactoring

Showing 61 changed files with 1103 additions and 335 deletions
@@ -455,6 +455,24 @@ @@ -455,6 +455,24 @@
455 "dataKeySettingsSchema": "{}\n", 455 "dataKeySettingsSchema": "{}\n",
456 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" 456 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
457 } 457 }
  458 + },
  459 + {
  460 + "alias": "update_json_attribute",
  461 + "name": "Update JSON attribute",
  462 + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADhCAMAAACZWwyuAAAB71BMVEX////w8PDt7e3z8/Pg4ODAwMDc3Nz19fUZhJQ4lKK9vb17e3v9/f2mpqbj4+P4+Pi+vr58fHzT09P7+/uTxcz2+/ucy9GpqakCAgK93OHNzc0eh5b09PSw1dusrKwvkZ+ampqysrIzMzPV1dXCwsKurq4iiZnZ2dnX19fIyMgrj50ABsiKiopmZmbo8/XQ5+ro6OjPz89JnqqCgoLX6u2cnJwmjJv39/ff39+12N7ExMQ2NjaioqKYmJjs7OzD4OTS0tLKysrh8PLq6uq6uroCCMjDw8OOjo6hzdO0tLQuj56UlJS4uvDZ7O7l5eVvsrw0k6EchZWEhIRwcHDU6ezG4eaGv8d1tb+3t7dCmqeIiIj4+/3j8fO62+AKD8tWpbGgoKBqamouLi7u7u47lqTP0PWr0tgXHM1/u8N6uMFqsLpfqrVNoKxFRUXU1faYyM92dnY7Ozvz+Pry8vK5ubm4uLh+fn5sbGzGx/OipOtzduG42t8uM9KNw8pTo690dHT6+vrt9viOkefb29um0NYfHx/p6vu9vvGqrO2YmulXW9uhztRnrrgmJiYQEBDt7fzk5frf4PjL4+eMj+a/3eJpbN9MUNk7P9SQxMuNwslfX19TU1OcnurI4uY9QdYyN9ImK9BVVVVPT097fuNYWFjSJwPyAAALvElEQVR42uzbTWviQBjA8ad5qgNONI05FCSIshUNKERCSFlM2WIxSKCH5FI8RfoFCrvVYwv7xTfG4susxqgpqJkfHoLM6c9MTCYROI7jOC4doiIRoqrEkhQRuIOJEsEVROI1Dw2pIkPlMQ8PyWMez8ItLOD2QnArAlxyIsEYhK/x5CSMJQGX0Cfu8AmXTCEqbkWUPQaBiDtd8hJXMJ6SbNByeWd3iROMRxIPAhEx09MSd1ATDwIJMdPTEndJNIid4D/kkLk4yMbFZTop2fXdonSZktb2XuFKpwRnBzeoGoeklNZS1tjj+BVu2hDKfYhxKXUXTtnXHIqgXd+esk/pNSK+GrQcjZsbuotBVrKUFmzSGs++b1djZqVV1m01D6crqlSpVOldpYKFSmzKWUCN0nI0bq5bWAwiuHquNNnj+JOlSDWA144OJW86KUcp38oAtQZIPZ/mICQEBvWGcLpw7i6acu0iCkXDtQ1hcos4MtdTOt4VYs2h5dk42y4azj3ejhYpVUyEwEZ1F+BpBJKviQVfWUkZuOTZz0e5ba8FJ4xNWZcLPw0De97vezpYTzmUK1iiL18p5T+aEaymxGRU2OhhTMAbgFISATvCMqU0LgEUo4U/KOreKV9LsSlHAeKtgY+09STr6ylvzABfRuWvlC5i4DCz8oiU4Az6Uwug25blzsMyZb8zDpkQkgj04YSxKSf16FwpTOquzZwrnx5lvT3sLlP21lKSoxY4aM4wANCn10DmKemvaFZ2HuBMMCnDT5QSe205x6R8ufICWqowKQ/92WFJ/lsTID+9vhp29FlKpye1/AbUXELqTTgDbMq60S2PDMRHKutsSqzTGrIpD74YYvUohBr+x1D+O0tZMqaO0wASfPjmWdwjbfgFd0wDUZjY+F/Ke6ptSRl/iZ6Nm3Dc4sq7Se/GMRtbQ7hZvjrR+XZGKimrhoZ7pOSbbKntV/Kt39R20fkDiZSf7Yi4i3rBkzLVJ46g4M7mHH+l4Bjpv+hiXfTyTr9lVk+U38Diqzs1n1l8XeibiJaKLP7O74FEiYdMT/RfE0RVJZICe8txSSRKmU9KEIRm8V0QnitNIWvST6m93whhy3/sm/lTEmEYx7/SOLPbdFgjCDGEWm0FBUVEGRRF0WEmlCgxeFSaeKHpNF1ajWW3V4cddh9/aLs078PqCMOCxjrs55dn3mff9/3hM7x7fGfYrSs1ll9leUWFrmrK+ERXahSk0uc9srTKJ7c31+hKjQJUHnnorc2g0lijK9eVGgWovHF1fUaV5zSVyg54RUaVNzWVearcK5FWWWPcqanMU2VjU1NTI6kcNjbpNJX5HvDGq43pA7510/Q+TaVylcyl/F65z1iuqVSi8vgpX23VqaUfO5c1lUpUrq+V2LXUh6PxnPbhuExxhnHrfi3OKFylbseV8ic/tz/WlRoroFK8V9ZMGad0pUZuKstyBSWMpnJ1qfS0tXmk6hhFHmRfR/sXnQJUVswPJy8dy0Vlvd9fL1WDFXmRfR3tv5hn39oT9+JU5TyyNwBQi8rOz3vWVnurc1HZ5XR2rZhK2n8x68buNbwNPqIq43lCVSrLpB/k1aZcVD5wODoAXOQk9ND3hD39DrzhzR39QgiIhs22Xj1YH4EQL5hjQIQ3C5OO9DrbWaCSZ9dpPu2PjdeuHZK7BPD+JdU0g4nrQVWplPizJxeVAaAVgLvF5na7gT6+rnlgAJZ6p6ujWfCD72uesEWpDxP3qvlrGzA+6hwNh2gdU8mus/m0PzYYjYexkO/3qBLxsbtQnUpf8vDCRi4HNcBFADcXsPDoDcF1FnwlYOFaWV9UpQdjiGfrSCW7TvPTVB/FQl4knlIlfr+H6lRWDZ8uU66yjjOLcM2Syj64DCmV41wd65PK1sl6m9WWUSXNz8zd9jtUiaftg6pTuXX4VFkeKk2c84KIXlTZE2IqRzgT65PKrgF/pEem0jpEKuX7ZDPZQDXNC3swGLSnnkNqUbnfW6PwFb1bgEgr1w0JUSXAVJq4AOszVSNcHTBho3VwRUkl7bOQg9WQMdj+nOo/vnwBMDMoEnwxA0AtKm/MbxE5qUBlhGt5Z9AjKtw3WSwylTFLd7gfrM9Uge/wO3ts6XWfhFf+PlJJ84nFj52XH66LxKli1m5/DQl2wM9MX1aFyl//8koFKvHRY3Y5sO5+2Gxtkau0CpMjYH1SdaFDMNf3p9fpo8LF+hBdp/nEopehH3aJ11QRHxuLL1D5y3hbFSqXAvnAV6JIbJuu1lQuC0eStRs1lctC1aU16njsaCFb8VRW5pCxvWnBauW/5ZXZDvKQJ0K6ednYMarGXHIFVFZ99ib36BTklRlVRjmTXCWNDdaccsmgXaIBSlGPSt9BMa/0KcgrM6rUOyFTSWNSmT2XxB2RucQsFKMalRLztxTklbxVEKJ6FktQXhngOK4OIiOTgjXGszHllA5p1Mo1L5FLErPtd6EcFansXDt8TkFeyff7J4RPTCXllXG3+2JKXX/YYhFVsjHllLFewCLEs+WSc2MoOoWoXFtbu6kzg8pM74+9LlJJeSWQUhfg/Kk+G9MBb+GB3g5kySWfioFF0SlE5b4TV4YvKVQ5ZCWVLK9k6tq4kaVVOjk3YkPIwtuECv4gV+C90uc9pkzlWRvljiyvZOpMmVSiq2WciyALH+ZQfApUuV65SsodKRkCPN2p/NGS6rMx5ZQYCk+Es+SSmAk+Q/EpQOX5mgpf8mGZYpUsd5SpfDBgMugR8hikPhtTTolxwdqDzLkkntlHUHzyV9l5I+lNHmhUrpLljjKVzpg55kC8x2a2hsDGlFMCvVwEmXNJPEpABayOOMPggvpZBSrHnRaPCepnBVRuOFFefmIDlg3TX/buXqVhKIzD+FuonEEJJQ4eKogEOrjFQRRSisEiuIl3YPYOvQId7Kg34OSdGgd1SaD5aPmH8zwQsv8g5Jx3OOf08c0GUP+UUezMXDzA8/3lKHNnZS630Oqf8sAK//5QvkKrG+VVsqygXEQvGyibUc5WlZTl44+gbEJ5u5pm1ZT3/hjKBpSLr5NRDeWFf4WyyR78eVRHeecnUG5PebOOaynt0K8ttNpTzpMsy5LkvIry+uNpZqHVnjK6LMvm4yrKqXd84A2X6PzBd0mZux/KERvHfsYZE884o5ch2/LscxOe5C4orfDFxMJrAFP0oQQllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBuWdK2qLwDj8lIiIiIiIKLpdG45ZFqTP6y407heV/aTfK1Oi3qBtlgNdmfLdrxypyw0AAhifJ2AeDZtcohUAEgZGFrdJVjIsgk2DY5urt9xFu9wn2xWMFLklzV3gb45u/GHD7YSML5s3wwUASSqHcbkIplNtLKIVyewll7onaZR7oB/yfJ/UYZfvLmbMXyscpy2CKxplOKB+mtBwRNRcflLJJvhh/Anybir4nBV9sMeJKyonnZaYO/cm4GX1+JC6xr02dPgAlpaFpFMTq7onUsRkx0WEd5RD42ucP3YWJeMJQI4YTJj7H2tz3TzkBlPR0oQEgkUK6AEzz2mPnxhx6LPsW0RA2XHYc8ebKwXPcP2UPgPTynAk9KV2pTHtcRZkxZ8Ma9dkZvqJmO3KLjnPFTimP1EEm/PyP8tMfSp8p7/R9FWWyy9BMrTnp1lwRA53zNx70UrtTSmjiMhIdXykvdADQpDCT2katoiTWiC1Xli0OmbJyZkQ8m4XR7/XYASQ7JNLwSglxfh4qUl+bETUNsIqyM66YAyfPdaz5hpiYuzzDWLHeKyUcYhEH9Zcy/wzZRApebDUOa2873dWZ2iJG54oQENHVuGRrE+Ju30q5g28qoRTK7SWUQrm9hFIot5dQCuX2kvUrWQrcXrKqKgvUkiRJkiRJkiS9328dj4CaN1dagAAAAABJRU5ErkJggg==",
  463 + "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.",
  464 + "descriptor": {
  465 + "type": "latest",
  466 + "sizeX": 7.5,
  467 + "sizeY": 3,
  468 + "resources": [],
  469 + "templateHtml": "<tb-json-input-widget \n [ctx]=\"ctx\">\n</tb-json-input-widget>",
  470 + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}",
  471 + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}",
  472 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}",
  473 + "dataKeySettingsSchema": "{}",
  474 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
  475 + }
458 } 476 }
459 ] 477 ]
460 } 478 }
@@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
21 @Controller 21 @Controller
22 public class WebConfig { 22 public class WebConfig {
23 23
24 - @RequestMapping(value = "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**") 24 + @RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**"})
25 public String redirect() { 25 public String redirect() {
26 return "forward:/index.html"; 26 return "forward:/index.html";
27 } 27 }
@@ -17,8 +17,10 @@ package org.thingsboard.server.service.install; @@ -17,8 +17,10 @@ package org.thingsboard.server.service.install;
17 17
18 import com.fasterxml.jackson.databind.ObjectMapper; 18 import com.fasterxml.jackson.databind.ObjectMapper;
19 import com.fasterxml.jackson.databind.node.ObjectNode; 19 import com.fasterxml.jackson.databind.node.ObjectNode;
  20 +import lombok.Getter;
20 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
21 import org.springframework.beans.factory.annotation.Autowired; 22 import org.springframework.beans.factory.annotation.Autowired;
  23 +import org.springframework.beans.factory.annotation.Value;
22 import org.springframework.context.annotation.Bean; 24 import org.springframework.context.annotation.Bean;
23 import org.springframework.context.annotation.Profile; 25 import org.springframework.context.annotation.Profile;
24 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 26 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -40,7 +40,7 @@ import org.thingsboard.server.transport.lwm2m.utils.TypeServer; @@ -40,7 +40,7 @@ import org.thingsboard.server.transport.lwm2m.utils.TypeServer;
40 40
41 import java.io.IOException; 41 import java.io.IOException;
42 import java.security.GeneralSecurityException; 42 import java.security.GeneralSecurityException;
43 -import java.util.Arrays; 43 +import java.util.Collections;
44 import java.util.List; 44 import java.util.List;
45 import java.util.UUID; 45 import java.util.UUID;
46 46
@@ -70,8 +70,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { @@ -70,8 +70,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
70 70
71 @Override 71 @Override
72 public List<SecurityInfo> getAllByEndpoint(String endPoint) { 72 public List<SecurityInfo> getAllByEndpoint(String endPoint) {
73 - String endPointKey = endPoint;  
74 - ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPointKey, TypeServer.BOOTSTRAP); 73 + ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPoint, TypeServer.BOOTSTRAP);
75 if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { 74 if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) {
76 /** add value to store from BootstrapJson */ 75 /** add value to store from BootstrapJson */
77 this.setBootstrapConfigScurityInfo(store); 76 this.setBootstrapConfigScurityInfo(store);
@@ -87,7 +86,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { @@ -87,7 +86,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
87 } catch (InvalidConfigurationException e) { 86 } catch (InvalidConfigurationException e) {
88 log.error("", e); 87 log.error("", e);
89 } 88 }
90 - return store.getSecurityInfo() == null ? null : Arrays.asList(store.getSecurityInfo()); 89 + return store.getSecurityInfo() == null ? null : Collections.singletonList(store.getSecurityInfo());
91 } 90 }
92 } 91 }
93 return null; 92 return null;
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java renamed from ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss
@@ -13,7 +13,8 @@ @@ -13,7 +13,8 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
  16 +package org.thingsboard.server.transport.lwm2m.server;
16 17
17 -:host {  
18 - min-width: 364px; 18 +public interface LwM2mQueuedRequest {
  19 + void send();
19 } 20 }
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 package org.thingsboard.server.transport.lwm2m.server; 16 package org.thingsboard.server.transport.lwm2m.server;
17 17
18 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
  19 +import org.eclipse.californium.core.coap.CoAP;
19 import org.eclipse.californium.core.coap.Response; 20 import org.eclipse.californium.core.coap.Response;
20 import org.eclipse.leshan.core.attributes.Attribute; 21 import org.eclipse.leshan.core.attributes.Attribute;
21 import org.eclipse.leshan.core.attributes.AttributeSet; 22 import org.eclipse.leshan.core.attributes.AttributeSet;
@@ -34,6 +35,7 @@ import org.eclipse.leshan.core.request.ObserveRequest; @@ -34,6 +35,7 @@ import org.eclipse.leshan.core.request.ObserveRequest;
34 import org.eclipse.leshan.core.request.ReadRequest; 35 import org.eclipse.leshan.core.request.ReadRequest;
35 import org.eclipse.leshan.core.request.WriteAttributesRequest; 36 import org.eclipse.leshan.core.request.WriteAttributesRequest;
36 import org.eclipse.leshan.core.request.WriteRequest; 37 import org.eclipse.leshan.core.request.WriteRequest;
  38 +import org.eclipse.leshan.core.request.exception.ClientSleepingException;
37 import org.eclipse.leshan.core.response.CancelObservationResponse; 39 import org.eclipse.leshan.core.response.CancelObservationResponse;
38 import org.eclipse.leshan.core.response.DeleteResponse; 40 import org.eclipse.leshan.core.response.DeleteResponse;
39 import org.eclipse.leshan.core.response.DiscoverResponse; 41 import org.eclipse.leshan.core.response.DiscoverResponse;
@@ -58,7 +60,6 @@ import java.util.Date; @@ -58,7 +60,6 @@ import java.util.Date;
58 import java.util.concurrent.ExecutorService; 60 import java.util.concurrent.ExecutorService;
59 import java.util.concurrent.Executors; 61 import java.util.concurrent.Executors;
60 62
61 -import static org.eclipse.californium.core.coap.CoAP.ResponseCode.isSuccess;  
62 import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; 63 import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD;
63 import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT; 64 import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT;
64 import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER; 65 import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER;
@@ -215,13 +216,19 @@ public class LwM2mTransportRequest { @@ -215,13 +216,19 @@ public class LwM2mTransportRequest {
215 request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet); 216 request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet);
216 } 217 }
217 break; 218 break;
218 - default:  
219 } 219 }
220 220
221 if (request != null) { 221 if (request != null) {
222 - this.sendRequest(registration, lwM2MClient, request, timeoutInMs);  
223 - }  
224 - else { 222 + try {
  223 + this.sendRequest(registration, lwM2MClient, request, timeoutInMs);
  224 + } catch (ClientSleepingException e) {
  225 + DownlinkRequest finalRequest = request;
  226 + long finalTimeoutInMs = timeoutInMs;
  227 + lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs));
  228 + } catch (Exception e) {
  229 + log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper, e);
  230 + }
  231 + } else {
225 log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer); 232 log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer);
226 } 233 }
227 } 234 }
@@ -240,7 +247,7 @@ public class LwM2mTransportRequest { @@ -240,7 +247,7 @@ public class LwM2mTransportRequest {
240 if (!lwM2MClient.isInit()) { 247 if (!lwM2MClient.isInit()) {
241 lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); 248 lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration));
242 } 249 }
243 - if (isSuccess(((Response) response.getCoapResponse()).getCode())) { 250 + if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) {
244 this.handleResponse(registration, request.getPath().toString(), response, request); 251 this.handleResponse(registration, request.getPath().toString(), response, request);
245 if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) { 252 if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) {
246 String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client", 253 String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client",
@@ -283,7 +290,7 @@ public class LwM2mTransportRequest { @@ -283,7 +290,7 @@ public class LwM2mTransportRequest {
283 case FLOAT: // Double 290 case FLOAT: // Double
284 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString())); 291 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString()));
285 case TIME: // Date 292 case TIME: // Date
286 - Date date = new Date(Long.decode(value.toString())); 293 + Date date = new Date(Long.decode(value.toString()));
287 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date); 294 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date);
288 case OPAQUE: // byte[] value, base64 295 case OPAQUE: // byte[] value, base64
289 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())); 296 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray()));
@@ -146,9 +146,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @@ -146,9 +146,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
146 * Next -> Create new LwM2MClient for current session -> setModelClient... 146 * Next -> Create new LwM2MClient for current session -> setModelClient...
147 * 147 *
148 * @param registration - Registration LwM2M Client 148 * @param registration - Registration LwM2M Client
149 - * @param previousObsersations - may be null 149 + * @param previousObservations - may be null
150 */ 150 */
151 - public void onRegistered(Registration registration, Collection<Observation> previousObsersations) { 151 + public void onRegistered(Registration registration, Collection<Observation> previousObservations) {
152 executorRegistered.submit(() -> { 152 executorRegistered.submit(() -> {
153 try { 153 try {
154 log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId()); 154 log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId());
@@ -188,6 +188,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @@ -188,6 +188,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
188 LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo); 188 LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo);
189 if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) { 189 if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) {
190 initLwM2mClient(lwM2MClient, sessionInfo); 190 initLwM2mClient(lwM2MClient, sessionInfo);
  191 + } else {
  192 + if (registration.getBindingMode().useQueueMode()) {
  193 + LwM2mQueuedRequest request;
  194 + while ((request = lwM2MClient.getQueuedRequests().poll()) != null) {
  195 + request.send();
  196 + }
  197 + }
191 } 198 }
192 199
193 log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); 200 log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType());
@@ -206,7 +213,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @@ -206,7 +213,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
206 * !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect 213 * !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect
207 */ 214 */
208 public void unReg(Registration registration, Collection<Observation> observations) { 215 public void unReg(Registration registration, Collection<Observation> observations) {
209 - executorUnRegistered.submit(() -> { 216 + executorUnRegistered.submit(() -> {
210 try { 217 try {
211 this.setCancelObservations(registration); 218 this.setCancelObservations(registration);
212 this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration); 219 this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration);
@@ -239,8 +246,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @@ -239,8 +246,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
239 } 246 }
240 } 247 }
241 248
  249 + @Override
242 public void onSleepingDev(Registration registration) { 250 public void onSleepingDev(Registration registration) {
243 log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint()); 251 log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint());
  252 + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is sleeping!", registration);
  253 +
244 //TODO: associate endpointId with device information. 254 //TODO: associate endpointId with device information.
245 } 255 }
246 256
@@ -417,6 +427,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @@ -417,6 +427,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
417 */ 427 */
418 protected void onAwakeDev(Registration registration) { 428 protected void onAwakeDev(Registration registration) {
419 log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint()); 429 log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint());
  430 + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is awake!", registration);
420 //TODO: associate endpointId with device information. 431 //TODO: associate endpointId with device information.
421 } 432 }
422 433
@@ -612,7 +623,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @@ -612,7 +623,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
612 if (GET_TYPE_OPER_READ.equals(typeOper)) { 623 if (GET_TYPE_OPER_READ.equals(typeOper)) {
613 result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() { 624 result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() {
614 }); 625 });
615 - result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() { 626 + result.addAll(JacksonUtil.convertValue(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() {
616 })); 627 }));
617 } else { 628 } else {
618 result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() { 629 result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() {
@@ -25,13 +25,16 @@ import org.eclipse.leshan.server.registration.Registration; @@ -25,13 +25,16 @@ import org.eclipse.leshan.server.registration.Registration;
25 import org.eclipse.leshan.server.security.SecurityInfo; 25 import org.eclipse.leshan.server.security.SecurityInfo;
26 import org.thingsboard.server.gen.transport.TransportProtos; 26 import org.thingsboard.server.gen.transport.TransportProtos;
27 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; 27 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
  28 +import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest;
28 import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl; 29 import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl;
29 30
30 import java.util.List; 31 import java.util.List;
31 import java.util.Map; 32 import java.util.Map;
  33 +import java.util.Queue;
32 import java.util.Set; 34 import java.util.Set;
33 import java.util.UUID; 35 import java.util.UUID;
34 import java.util.concurrent.ConcurrentHashMap; 36 import java.util.concurrent.ConcurrentHashMap;
  37 +import java.util.concurrent.ConcurrentLinkedQueue;
35 import java.util.concurrent.CopyOnWriteArrayList; 38 import java.util.concurrent.CopyOnWriteArrayList;
36 import java.util.stream.Collectors; 39 import java.util.stream.Collectors;
37 40
@@ -54,6 +57,7 @@ public class LwM2mClient implements Cloneable { @@ -54,6 +57,7 @@ public class LwM2mClient implements Cloneable {
54 private final Map<String, ResourceValue> resources; 57 private final Map<String, ResourceValue> resources;
55 private final Map<String, TransportProtos.TsKvProto> delayedRequests; 58 private final Map<String, TransportProtos.TsKvProto> delayedRequests;
56 private final List<String> pendingRequests; 59 private final List<String> pendingRequests;
  60 + private final Queue<LwM2mQueuedRequest> queuedRequests;
57 private boolean init; 61 private boolean init;
58 62
59 public Object clone() throws CloneNotSupportedException { 63 public Object clone() throws CloneNotSupportedException {
@@ -71,6 +75,7 @@ public class LwM2mClient implements Cloneable { @@ -71,6 +75,7 @@ public class LwM2mClient implements Cloneable {
71 this.profileId = profileId; 75 this.profileId = profileId;
72 this.sessionId = sessionId; 76 this.sessionId = sessionId;
73 this.init = false; 77 this.init = false;
  78 + this.queuedRequests = new ConcurrentLinkedQueue<>();
74 } 79 }
75 80
76 public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) { 81 public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) {
@@ -51,11 +51,11 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com @@ -51,11 +51,11 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
51 TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); 51 TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
52 try { 52 try {
53 componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); 53 componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
  54 + transactionManager.commit(transaction);
54 } catch (Throwable th) { 55 } catch (Throwable th) {
55 log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); 56 log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType());
56 transactionManager.rollback(transaction); 57 transactionManager.rollback(transaction);
57 } 58 }
58 - transactionManager.commit(transaction);  
59 } else { 59 } else {
60 log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); 60 log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType());
61 } 61 }
@@ -51,11 +51,11 @@ public abstract class AbstractEventInsertRepository implements EventInsertReposi @@ -51,11 +51,11 @@ public abstract class AbstractEventInsertRepository implements EventInsertReposi
51 TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); 51 TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
52 try { 52 try {
53 eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); 53 eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
  54 + transactionManager.commit(transaction);
54 } catch (Throwable th) { 55 } catch (Throwable th) {
55 log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); 56 log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
56 transactionManager.rollback(transaction); 57 transactionManager.rollback(transaction);
57 } 58 }
58 - transactionManager.commit(transaction);  
59 } else { 59 } else {
60 log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); 60 log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
61 } 61 }
@@ -62,6 +62,8 @@ import java.util.Collections; @@ -62,6 +62,8 @@ import java.util.Collections;
62 import java.util.List; 62 import java.util.List;
63 import java.util.Optional; 63 import java.util.Optional;
64 import java.util.concurrent.TimeUnit; 64 import java.util.concurrent.TimeUnit;
  65 +import java.util.concurrent.locks.Lock;
  66 +import java.util.concurrent.locks.ReentrantLock;
65 import java.util.stream.Collectors; 67 import java.util.stream.Collectors;
66 68
67 import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; 69 import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
@@ -107,6 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -107,6 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
107 private PreparedStatement[] fetchStmtsDesc; 109 private PreparedStatement[] fetchStmtsDesc;
108 private PreparedStatement deleteStmt; 110 private PreparedStatement deleteStmt;
109 private PreparedStatement deletePartitionStmt; 111 private PreparedStatement deletePartitionStmt;
  112 + private final Lock stmtCreationLock = new ReentrantLock();
110 113
111 private boolean isInstall() { 114 private boolean isInstall() {
112 return environment.acceptsProfiles(Profiles.of("install")); 115 return environment.acceptsProfiles(Profiles.of("install"));
@@ -545,13 +548,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -545,13 +548,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
545 548
546 private PreparedStatement getDeleteStmt() { 549 private PreparedStatement getDeleteStmt() {
547 if (deleteStmt == null) { 550 if (deleteStmt == null) {
548 - deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF +  
549 - " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM  
550 - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM  
551 - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM  
552 - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM  
553 - + "AND " + ModelConstants.TS_COLUMN + " >= ? "  
554 - + "AND " + ModelConstants.TS_COLUMN + " < ?"); 551 + stmtCreationLock.lock();
  552 + try {
  553 + if (deleteStmt == null) {
  554 + deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF +
  555 + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
  556 + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
  557 + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
  558 + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
  559 + + "AND " + ModelConstants.TS_COLUMN + " >= ? "
  560 + + "AND " + ModelConstants.TS_COLUMN + " < ?");
  561 + }
  562 + } finally {
  563 + stmtCreationLock.unlock();
  564 + }
555 } 565 }
556 return deleteStmt; 566 return deleteStmt;
557 } 567 }
@@ -585,27 +595,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -585,27 +595,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
585 595
586 private PreparedStatement getDeletePartitionStmt() { 596 private PreparedStatement getDeletePartitionStmt() {
587 if (deletePartitionStmt == null) { 597 if (deletePartitionStmt == null) {
588 - deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF +  
589 - " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM  
590 - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM  
591 - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM  
592 - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); 598 + stmtCreationLock.lock();
  599 + try {
  600 + if (deletePartitionStmt == null) {
  601 + deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF +
  602 + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
  603 + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
  604 + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
  605 + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM);
  606 + }
  607 + } finally {
  608 + stmtCreationLock.unlock();
  609 + }
593 } 610 }
594 return deletePartitionStmt; 611 return deletePartitionStmt;
595 } 612 }
596 613
597 private PreparedStatement getSaveStmt(DataType dataType) { 614 private PreparedStatement getSaveStmt(DataType dataType) {
598 if (saveStmts == null) { 615 if (saveStmts == null) {
599 - saveStmts = new PreparedStatement[DataType.values().length];  
600 - for (DataType type : DataType.values()) {  
601 - saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +  
602 - "(" + ModelConstants.ENTITY_TYPE_COLUMN +  
603 - "," + ModelConstants.ENTITY_ID_COLUMN +  
604 - "," + ModelConstants.KEY_COLUMN +  
605 - "," + ModelConstants.PARTITION_COLUMN +  
606 - "," + ModelConstants.TS_COLUMN +  
607 - "," + getColumnName(type) + ")" +  
608 - " VALUES(?, ?, ?, ?, ?, ?)"); 616 + stmtCreationLock.lock();
  617 + try {
  618 + if (saveStmts == null) {
  619 + saveStmts = new PreparedStatement[DataType.values().length];
  620 + for (DataType type : DataType.values()) {
  621 + saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
  622 + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
  623 + "," + ModelConstants.ENTITY_ID_COLUMN +
  624 + "," + ModelConstants.KEY_COLUMN +
  625 + "," + ModelConstants.PARTITION_COLUMN +
  626 + "," + ModelConstants.TS_COLUMN +
  627 + "," + getColumnName(type) + ")" +
  628 + " VALUES(?, ?, ?, ?, ?, ?)");
  629 + }
  630 + }
  631 + } finally {
  632 + stmtCreationLock.unlock();
609 } 633 }
610 } 634 }
611 return saveStmts[dataType.ordinal()]; 635 return saveStmts[dataType.ordinal()];
@@ -613,16 +637,23 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -613,16 +637,23 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
613 637
614 private PreparedStatement getSaveTtlStmt(DataType dataType) { 638 private PreparedStatement getSaveTtlStmt(DataType dataType) {
615 if (saveTtlStmts == null) { 639 if (saveTtlStmts == null) {
616 - saveTtlStmts = new PreparedStatement[DataType.values().length];  
617 - for (DataType type : DataType.values()) {  
618 - saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +  
619 - "(" + ModelConstants.ENTITY_TYPE_COLUMN +  
620 - "," + ModelConstants.ENTITY_ID_COLUMN +  
621 - "," + ModelConstants.KEY_COLUMN +  
622 - "," + ModelConstants.PARTITION_COLUMN +  
623 - "," + ModelConstants.TS_COLUMN +  
624 - "," + getColumnName(type) + ")" +  
625 - " VALUES(?, ?, ?, ?, ?, ?) USING TTL ?"); 640 + stmtCreationLock.lock();
  641 + try {
  642 + if (saveTtlStmts == null) {
  643 + saveTtlStmts = new PreparedStatement[DataType.values().length];
  644 + for (DataType type : DataType.values()) {
  645 + saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
  646 + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
  647 + "," + ModelConstants.ENTITY_ID_COLUMN +
  648 + "," + ModelConstants.KEY_COLUMN +
  649 + "," + ModelConstants.PARTITION_COLUMN +
  650 + "," + ModelConstants.TS_COLUMN +
  651 + "," + getColumnName(type) + ")" +
  652 + " VALUES(?, ?, ?, ?, ?, ?) USING TTL ?");
  653 + }
  654 + }
  655 + } finally {
  656 + stmtCreationLock.unlock();
626 } 657 }
627 } 658 }
628 return saveTtlStmts[dataType.ordinal()]; 659 return saveTtlStmts[dataType.ordinal()];
@@ -630,24 +661,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -630,24 +661,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
630 661
631 private PreparedStatement getPartitionInsertStmt() { 662 private PreparedStatement getPartitionInsertStmt() {
632 if (partitionInsertStmt == null) { 663 if (partitionInsertStmt == null) {
633 - partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +  
634 - "(" + ModelConstants.ENTITY_TYPE_COLUMN +  
635 - "," + ModelConstants.ENTITY_ID_COLUMN +  
636 - "," + ModelConstants.PARTITION_COLUMN +  
637 - "," + ModelConstants.KEY_COLUMN + ")" +  
638 - " VALUES(?, ?, ?, ?)"); 664 + stmtCreationLock.lock();
  665 + try {
  666 + if (partitionInsertStmt == null) {
  667 + partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
  668 + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
  669 + "," + ModelConstants.ENTITY_ID_COLUMN +
  670 + "," + ModelConstants.PARTITION_COLUMN +
  671 + "," + ModelConstants.KEY_COLUMN + ")" +
  672 + " VALUES(?, ?, ?, ?)");
  673 + }
  674 + } finally {
  675 + stmtCreationLock.unlock();
  676 + }
639 } 677 }
640 return partitionInsertStmt; 678 return partitionInsertStmt;
641 } 679 }
642 680
643 private PreparedStatement getPartitionInsertTtlStmt() { 681 private PreparedStatement getPartitionInsertTtlStmt() {
644 if (partitionInsertTtlStmt == null) { 682 if (partitionInsertTtlStmt == null) {
645 - partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +  
646 - "(" + ModelConstants.ENTITY_TYPE_COLUMN +  
647 - "," + ModelConstants.ENTITY_ID_COLUMN +  
648 - "," + ModelConstants.PARTITION_COLUMN +  
649 - "," + ModelConstants.KEY_COLUMN + ")" +  
650 - " VALUES(?, ?, ?, ?) USING TTL ?"); 683 + stmtCreationLock.lock();
  684 + try {
  685 + if (partitionInsertTtlStmt == null) {
  686 + partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
  687 + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
  688 + "," + ModelConstants.ENTITY_ID_COLUMN +
  689 + "," + ModelConstants.PARTITION_COLUMN +
  690 + "," + ModelConstants.KEY_COLUMN + ")" +
  691 + " VALUES(?, ?, ?, ?) USING TTL ?");
  692 + }
  693 + } finally {
  694 + stmtCreationLock.unlock();
  695 + }
651 } 696 }
652 return partitionInsertTtlStmt; 697 return partitionInsertTtlStmt;
653 } 698 }
@@ -713,12 +758,26 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -713,12 +758,26 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
713 switch (orderBy) { 758 switch (orderBy) {
714 case ASC_ORDER: 759 case ASC_ORDER:
715 if (fetchStmtsAsc == null) { 760 if (fetchStmtsAsc == null) {
716 - fetchStmtsAsc = initFetchStmt(orderBy); 761 + stmtCreationLock.lock();
  762 + try {
  763 + if (fetchStmtsAsc == null) {
  764 + fetchStmtsAsc = initFetchStmt(orderBy);
  765 + }
  766 + } finally {
  767 + stmtCreationLock.unlock();
  768 + }
717 } 769 }
718 return fetchStmtsAsc[aggType.ordinal()]; 770 return fetchStmtsAsc[aggType.ordinal()];
719 case DESC_ORDER: 771 case DESC_ORDER:
720 if (fetchStmtsDesc == null) { 772 if (fetchStmtsDesc == null) {
721 - fetchStmtsDesc = initFetchStmt(orderBy); 773 + stmtCreationLock.lock();
  774 + try {
  775 + if (fetchStmtsDesc == null) {
  776 + fetchStmtsDesc = initFetchStmt(orderBy);
  777 + }
  778 + } finally {
  779 + stmtCreationLock.unlock();
  780 + }
722 } 781 }
723 return fetchStmtsDesc[aggType.ordinal()]; 782 return fetchStmtsDesc[aggType.ordinal()];
724 default: 783 default:
@@ -10,6 +10,7 @@ To run the black box tests with using Docker, the local Docker images of Thingsb @@ -10,6 +10,7 @@ To run the black box tests with using Docker, the local Docker images of Thingsb
10 As result, in REPOSITORY column, next images should be present: 10 As result, in REPOSITORY column, next images should be present:
11 11
12 thingsboard/tb-coap-transport 12 thingsboard/tb-coap-transport
  13 + thingsboard/tb-lwm2m-transport
13 thingsboard/tb-http-transport 14 thingsboard/tb-http-transport
14 thingsboard/tb-mqtt-transport 15 thingsboard/tb-mqtt-transport
15 thingsboard/tb-node 16 thingsboard/tb-node
@@ -17,15 +17,14 @@ @@ -17,15 +17,14 @@
17 FROM thingsboard/openjdk11 17 FROM thingsboard/openjdk11
18 18
19 RUN apt-get update 19 RUN apt-get update
20 -RUN apt-get install -y curl nmap procps  
21 -RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null  
22 -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null 20 +RUN apt-get install -y curl nmap procps gnupg2
  21 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
23 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
24 RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null 23 RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
25 RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add - 24 RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add -
26 -ENV PG_MAJOR=11 25 +ENV PG_MAJOR=12
27 RUN apt-get update 26 RUN apt-get update
28 -RUN apt-get install -y cassandra cassandra-tools postgresql-11 27 +RUN apt-get install -y cassandra cassandra-tools postgresql-12
29 RUN update-rc.d cassandra disable 28 RUN update-rc.d cassandra disable
30 RUN update-rc.d postgresql disable 29 RUN update-rc.d postgresql disable
31 RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra 30 RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra
@@ -17,13 +17,12 @@ @@ -17,13 +17,12 @@
17 FROM thingsboard/openjdk11 17 FROM thingsboard/openjdk11
18 18
19 RUN apt-get update 19 RUN apt-get update
20 -RUN apt-get install -y curl  
21 -RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null  
22 -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null 20 +RUN apt-get install -y curl gnupg2
  21 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
23 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
24 -ENV PG_MAJOR 11 23 +ENV PG_MAJOR 12
25 RUN apt-get update 24 RUN apt-get update
26 -RUN apt-get install -y postgresql-11 25 +RUN apt-get install -y postgresql-12
27 RUN update-rc.d postgresql disable 26 RUN update-rc.d postgresql disable
28 27
29 COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ 28 COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/
@@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
39 <tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name> 39 <tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
40 <tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name> 40 <tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
41 <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> 41 <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
42 - <pkg.upgradeVersion>2.4.2</pkg.upgradeVersion> 42 + <pkg.upgradeVersion>3.3.0</pkg.upgradeVersion>
43 </properties> 43 </properties>
44 44
45 <dependencies> 45 <dependencies>
@@ -99,7 +99,7 @@ @@ -99,7 +99,7 @@
99 org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 99 org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
100 </sonar.exclusions> 100 </sonar.exclusions>
101 <elasticsearch.version>5.0.2</elasticsearch.version> 101 <elasticsearch.version>5.0.2</elasticsearch.version>
102 - <delight-nashorn-sandbox.version>0.1.31</delight-nashorn-sandbox.version> 102 + <delight-nashorn-sandbox.version>0.1.16</delight-nashorn-sandbox.version>
103 <kafka.version>2.6.0</kafka.version> 103 <kafka.version>2.6.0</kafka.version>
104 <bucket4j.version>4.1.1</bucket4j.version> 104 <bucket4j.version>4.1.1</bucket4j.version>
105 <fst.version>2.57</fst.version> 105 <fst.version>2.57</fst.version>
@@ -1338,7 +1338,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -1338,7 +1338,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1338 HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody(); 1338 HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody();
1339 } 1339 }
1340 1340
1341 - public PageData<DeviceProfile> getTenantDevices(PageLink pageLink) { 1341 + public PageData<DeviceProfile> getDeviceProfiles(PageLink pageLink) {
1342 Map<String, String> params = new HashMap<>(); 1342 Map<String, String> params = new HashMap<>();
1343 addPageLinkToParam(params, pageLink); 1343 addPageLinkToParam(params, pageLink);
1344 return restTemplate.exchange( 1344 return restTemplate.exchange(
@@ -18,10 +18,10 @@ import { Injectable } from '@angular/core'; @@ -18,10 +18,10 @@ import { Injectable } from '@angular/core';
18 import { HttpClient } from '@angular/common/http'; 18 import { HttpClient } from '@angular/common/http';
19 import { PageLink } from '@shared/models/page/page-link'; 19 import { PageLink } from '@shared/models/page/page-link';
20 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; 20 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
21 -import { Observable } from 'rxjs'; 21 +import { forkJoin, Observable, of } from 'rxjs';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
23 import { Resource, ResourceInfo } from '@shared/models/resource.models'; 23 import { Resource, ResourceInfo } from '@shared/models/resource.models';
24 -import { map } from 'rxjs/operators'; 24 +import { catchError, map, mergeMap } from 'rxjs/operators';
25 25
26 @Injectable({ 26 @Injectable({
27 providedIn: 'root' 27 providedIn: 'root'
@@ -70,6 +70,25 @@ export class ResourceService { @@ -70,6 +70,25 @@ export class ResourceService {
70 ); 70 );
71 } 71 }
72 72
  73 + public saveResources(resources: Resource[], config?: RequestConfig): Observable<Resource[]> {
  74 + let partSize = 100;
  75 + partSize = resources.length > partSize ? partSize : resources.length;
  76 + const resourceObservables: Observable<Resource>[] = [];
  77 + for (let i = 0; i < partSize; i++) {
  78 + resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(() => of({} as Resource))));
  79 + }
  80 + return forkJoin(resourceObservables).pipe(
  81 + mergeMap((resource) => {
  82 + resources.splice(0, partSize);
  83 + if (resources.length) {
  84 + return this.saveResources(resources, config);
  85 + } else {
  86 + return of(resource);
  87 + }
  88 + })
  89 + );
  90 + }
  91 +
73 public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> { 92 public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
74 return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config)); 93 return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
75 } 94 }
@@ -79,7 +79,7 @@ export class TimeService { @@ -79,7 +79,7 @@ export class TimeService {
79 79
80 public boundMinInterval(min: number): number { 80 public boundMinInterval(min: number): number {
81 if (isDefined(min)) { 81 if (isDefined(min)) {
82 - min = Math.floor(min / 1000) * 1000; 82 + min = Math.ceil(min / 1000) * 1000;
83 } 83 }
84 return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); 84 return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL);
85 } 85 }
@@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean { @@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean {
127 return true; 127 return true;
128 } 128 }
129 129
  130 +export function isLiteralObject(value: any) {
  131 + return (!!value) && (value.constructor === Object);
  132 +}
  133 +
130 export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { 134 export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
131 if (isDefinedAndNotNull(value) && isNumeric(value) && 135 if (isDefinedAndNotNull(value) && isNumeric(value) &&
132 (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { 136 (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) {
@@ -90,7 +90,6 @@ import { @@ -90,7 +90,6 @@ import {
90 } from '@home/components/alias/entity-aliases-dialog.component'; 90 } from '@home/components/alias/entity-aliases-dialog.component';
91 import { EntityAliases } from '@app/shared/models/alias.models'; 91 import { EntityAliases } from '@app/shared/models/alias.models';
92 import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component'; 92 import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component';
93 -import { WidgetsBundle } from '@shared/models/widgets-bundle.model';  
94 import { 93 import {
95 AddWidgetDialogComponent, 94 AddWidgetDialogComponent,
96 AddWidgetDialogData 95 AddWidgetDialogData
@@ -118,8 +117,7 @@ import { ComponentPortal } from '@angular/cdk/portal'; @@ -118,8 +117,7 @@ import { ComponentPortal } from '@angular/cdk/portal';
118 import { 117 import {
119 DISPLAY_WIDGET_TYPES_PANEL_DATA, 118 DISPLAY_WIDGET_TYPES_PANEL_DATA,
120 DisplayWidgetTypesPanelComponent, 119 DisplayWidgetTypesPanelComponent,
121 - DisplayWidgetTypesPanelData,  
122 - WidgetTypes 120 + DisplayWidgetTypesPanelData
123 } from '@home/components/dashboard-page/widget-types-panel.component'; 121 } from '@home/components/dashboard-page/widget-types-panel.component';
124 import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; 122 import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
125 import {AliasEntityType, EntityType} from "@shared/models/entity-type.models"; 123 import {AliasEntityType, EntityType} from "@shared/models/entity-type.models";
@@ -1189,13 +1187,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -1189,13 +1187,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
1189 overlayRef.dispose(); 1187 overlayRef.dispose();
1190 }); 1188 });
1191 1189
  1190 + const filterWidgetTypes = this.dashboardWidgetSelectComponent.filterWidgetTypes;
  1191 + const widgetTypesList = Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => {
  1192 + return {type, display: filterWidgetTypes === null ? true : filterWidgetTypes.includes(type)};
  1193 + });
  1194 +
1192 const providers: StaticProvider[] = [ 1195 const providers: StaticProvider[] = [
1193 { 1196 {
1194 provide: DISPLAY_WIDGET_TYPES_PANEL_DATA, 1197 provide: DISPLAY_WIDGET_TYPES_PANEL_DATA,
1195 useValue: { 1198 useValue: {
1196 - types: Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => {  
1197 - return {type, display: true};  
1198 - }), 1199 + types: widgetTypesList,
1199 typesUpdated: (newTypes) => { 1200 typesUpdated: (newTypes) => {
1200 this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type); 1201 this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type);
1201 } 1202 }
@@ -77,6 +77,10 @@ export class DashboardWidgetSelectComponent implements OnInit { @@ -77,6 +77,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
77 this.filterWidgetTypes$.next(widgetTypes); 77 this.filterWidgetTypes$.next(widgetTypes);
78 } 78 }
79 79
  80 + get filterWidgetTypes(): Array<widgetType> {
  81 + return this.filterWidgetTypes$.value;
  82 + }
  83 +
80 @Output() 84 @Output()
81 widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>(); 85 widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();
82 86
@@ -80,7 +80,7 @@ @@ -80,7 +80,7 @@
80 (mousedown)="widgetMouseDown($event, widget)" 80 (mousedown)="widgetMouseDown($event, widget)"
81 (click)="widgetClicked($event, widget)" 81 (click)="widgetClicked($event, widget)"
82 (contextmenu)="openWidgetContextMenu($event, widget)"> 82 (contextmenu)="openWidgetContextMenu($event, widget)">
83 - <div *ngIf="widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start"> 83 + <div *ngIf="!!widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start">
84 <div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel"> 84 <div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel">
85 <span *ngIf="widget.showTitle" 85 <span *ngIf="widget.showTitle"
86 [ngClass]="{'single-row': widget.hasTimewindow}" 86 [ngClass]="{'single-row': widget.hasTimewindow}"
@@ -15,11 +15,21 @@ @@ -15,11 +15,21 @@
15 /// 15 ///
16 16
17 import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; 18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
19 import { 28 import {
20 BooleanFilterPredicate, 29 BooleanFilterPredicate,
21 BooleanOperation, 30 BooleanOperation,
22 - booleanOperationTranslationMap, EntityKeyValueType, 31 + booleanOperationTranslationMap,
  32 + EntityKeyValueType,
23 FilterPredicateType 33 FilterPredicateType
24 } from '@shared/models/query/query.models'; 34 } from '@shared/models/query/query.models';
25 35
@@ -32,10 +42,15 @@ import { @@ -32,10 +42,15 @@ import {
32 provide: NG_VALUE_ACCESSOR, 42 provide: NG_VALUE_ACCESSOR,
33 useExisting: forwardRef(() => BooleanFilterPredicateComponent), 43 useExisting: forwardRef(() => BooleanFilterPredicateComponent),
34 multi: true 44 multi: true
  45 + },
  46 + {
  47 + provide: NG_VALIDATORS,
  48 + useExisting: forwardRef(() => BooleanFilterPredicateComponent),
  49 + multi: true
35 } 50 }
36 ] 51 ]
37 }) 52 })
38 -export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit { 53 +export class BooleanFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
39 54
40 @Input() disabled: boolean; 55 @Input() disabled: boolean;
41 56
@@ -73,7 +88,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On @@ -73,7 +88,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
73 registerOnTouched(fn: any): void { 88 registerOnTouched(fn: any): void {
74 } 89 }
75 90
76 - setDisabledState?(isDisabled: boolean): void { 91 + setDisabledState(isDisabled: boolean): void {
77 this.disabled = isDisabled; 92 this.disabled = isDisabled;
78 if (this.disabled) { 93 if (this.disabled) {
79 this.booleanFilterPredicateFormGroup.disable({emitEvent: false}); 94 this.booleanFilterPredicateFormGroup.disable({emitEvent: false});
@@ -82,17 +97,20 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On @@ -82,17 +97,20 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
82 } 97 }
83 } 98 }
84 99
  100 + validate(): ValidationErrors | null {
  101 + return this.booleanFilterPredicateFormGroup ? null : {
  102 + booleanFilterPredicate: {valid: false}
  103 + };
  104 + }
  105 +
85 writeValue(predicate: BooleanFilterPredicate): void { 106 writeValue(predicate: BooleanFilterPredicate): void {
86 this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); 107 this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
87 this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); 108 this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
88 } 109 }
89 110
90 private updateModel() { 111 private updateModel() {
91 - let predicate: BooleanFilterPredicate = null;  
92 - if (this.booleanFilterPredicateFormGroup.valid) {  
93 - predicate = this.booleanFilterPredicateFormGroup.getRawValue();  
94 - predicate.type = FilterPredicateType.BOOLEAN;  
95 - } 112 + const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue();
  113 + predicate.type = FilterPredicateType.BOOLEAN;
96 this.propagateChange(predicate); 114 this.propagateChange(predicate);
97 } 115 }
98 116
@@ -16,11 +16,7 @@ @@ -16,11 +16,7 @@
16 16
17 import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 18 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19 -import {  
20 - ComplexFilterPredicate,  
21 - ComplexFilterPredicateInfo,  
22 - EntityKeyValueType  
23 -} from '@shared/models/query/query.models'; 19 +import { ComplexFilterPredicateInfo, EntityKeyValueType } from '@shared/models/query/query.models';
24 import { MatDialog } from '@angular/material/dialog'; 20 import { MatDialog } from '@angular/material/dialog';
25 import { 21 import {
26 ComplexFilterPredicateDialogComponent, 22 ComplexFilterPredicateDialogComponent,
@@ -71,7 +67,7 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @@ -71,7 +67,7 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
71 registerOnTouched(fn: any): void { 67 registerOnTouched(fn: any): void {
72 } 68 }
73 69
74 - setDisabledState?(isDisabled: boolean): void { 70 + setDisabledState(isDisabled: boolean): void {
75 this.disabled = isDisabled; 71 this.disabled = isDisabled;
76 } 72 }
77 73
@@ -21,7 +21,10 @@ import { @@ -21,7 +21,10 @@ import {
21 FormArray, 21 FormArray,
22 FormBuilder, 22 FormBuilder,
23 FormGroup, 23 FormGroup,
  24 + NG_VALIDATORS,
24 NG_VALUE_ACCESSOR, 25 NG_VALUE_ACCESSOR,
  26 + ValidationErrors,
  27 + Validator,
25 Validators 28 Validators
26 } from '@angular/forms'; 29 } from '@angular/forms';
27 import { Observable, of, Subscription } from 'rxjs'; 30 import { Observable, of, Subscription } from 'rxjs';
@@ -49,10 +52,15 @@ import { map } from 'rxjs/operators'; @@ -49,10 +52,15 @@ import { map } from 'rxjs/operators';
49 provide: NG_VALUE_ACCESSOR, 52 provide: NG_VALUE_ACCESSOR,
50 useExisting: forwardRef(() => FilterPredicateListComponent), 53 useExisting: forwardRef(() => FilterPredicateListComponent),
51 multi: true 54 multi: true
  55 + },
  56 + {
  57 + provide: NG_VALIDATORS,
  58 + useExisting: forwardRef(() => FilterPredicateListComponent),
  59 + multi: true
52 } 60 }
53 ] 61 ]
54 }) 62 })
55 -export class FilterPredicateListComponent implements ControlValueAccessor, OnInit { 63 +export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit {
56 64
57 @Input() disabled: boolean; 65 @Input() disabled: boolean;
58 66
@@ -108,6 +116,12 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @@ -108,6 +116,12 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
108 } 116 }
109 } 117 }
110 118
  119 + validate(control: AbstractControl): ValidationErrors | null {
  120 + return this.filterListFormGroup.valid ? null : {
  121 + filterList: {valid: false}
  122 + };
  123 + }
  124 +
111 writeValue(predicates: Array<KeyFilterPredicateInfo>): void { 125 writeValue(predicates: Array<KeyFilterPredicateInfo>): void {
112 if (this.valueChangeSubscription) { 126 if (this.valueChangeSubscription) {
113 this.valueChangeSubscription.unsubscribe(); 127 this.valueChangeSubscription.unsubscribe();
@@ -178,7 +192,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @@ -178,7 +192,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
178 192
179 private updateModel() { 193 private updateModel() {
180 const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates; 194 const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
181 - if (this.filterListFormGroup.valid && predicates.length) { 195 + if (predicates.length) {
182 this.propagateChange(predicates); 196 this.propagateChange(predicates);
183 } else { 197 } else {
184 this.propagateChange(null); 198 this.propagateChange(null);
@@ -19,7 +19,10 @@ import { @@ -19,7 +19,10 @@ import {
19 ControlValueAccessor, 19 ControlValueAccessor,
20 FormBuilder, 20 FormBuilder,
21 FormGroup, 21 FormGroup,
  22 + NG_VALIDATORS,
22 NG_VALUE_ACCESSOR, 23 NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
23 ValidatorFn, 26 ValidatorFn,
24 Validators 27 Validators
25 } from '@angular/forms'; 28 } from '@angular/forms';
@@ -39,10 +42,15 @@ import { @@ -39,10 +42,15 @@ import {
39 provide: NG_VALUE_ACCESSOR, 42 provide: NG_VALUE_ACCESSOR,
40 useExisting: forwardRef(() => FilterPredicateValueComponent), 43 useExisting: forwardRef(() => FilterPredicateValueComponent),
41 multi: true 44 multi: true
  45 + },
  46 + {
  47 + provide: NG_VALIDATORS,
  48 + useExisting: forwardRef(() => FilterPredicateValueComponent),
  49 + multi: true
42 } 50 }
43 ] 51 ]
44 }) 52 })
45 -export class FilterPredicateValueComponent implements ControlValueAccessor, OnInit { 53 +export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit {
46 54
47 private readonly inheritModeForSources: DynamicValueSourceType[] = [ 55 private readonly inheritModeForSources: DynamicValueSourceType[] = [
48 DynamicValueSourceType.CURRENT_CUSTOMER, 56 DynamicValueSourceType.CURRENT_CUSTOMER,
@@ -62,7 +70,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @@ -62,7 +70,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
62 } 70 }
63 } 71 }
64 72
65 - @Input() onlyUserDynamicSource = false; 73 + private onlyUserDynamicSourceValue = false;
  74 +
  75 + @Input()
  76 + set onlyUserDynamicSource(dynamicMode: boolean) {
  77 + this.onlyUserDynamicSourceValue = dynamicMode;
  78 + if (this.filterPredicateValueFormGroup) {
  79 + this.updateValidationDynamicMode();
  80 + setTimeout(() => {
  81 + this.updateModel();
  82 + }, 0);
  83 + }
  84 + }
  85 +
  86 + get onlyUserDynamicSource(): boolean {
  87 + return this.onlyUserDynamicSourceValue;
  88 + }
66 89
67 @Input() 90 @Input()
68 valueType: EntityKeyValueType; 91 valueType: EntityKeyValueType;
@@ -83,6 +106,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @@ -83,6 +106,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
83 allow = true; 106 allow = true;
84 107
85 private propagateChange = null; 108 private propagateChange = null;
  109 + private propagateChangePending = false;
86 110
87 constructor(private fb: FormBuilder) { 111 constructor(private fb: FormBuilder) {
88 } 112 }
@@ -126,6 +150,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @@ -126,6 +150,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
126 this.updateShowInheritMode(sourceType); 150 this.updateShowInheritMode(sourceType);
127 } 151 }
128 ); 152 );
  153 + this.updateValidationDynamicMode();
129 this.filterPredicateValueFormGroup.valueChanges.subscribe(() => { 154 this.filterPredicateValueFormGroup.valueChanges.subscribe(() => {
130 this.updateModel(); 155 this.updateModel();
131 }); 156 });
@@ -133,12 +158,18 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @@ -133,12 +158,18 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
133 158
134 registerOnChange(fn: any): void { 159 registerOnChange(fn: any): void {
135 this.propagateChange = fn; 160 this.propagateChange = fn;
  161 + if (this.propagateChangePending) {
  162 + this.propagateChangePending = false;
  163 + setTimeout(() => {
  164 + this.updateModel();
  165 + }, 0);
  166 + }
136 } 167 }
137 168
138 registerOnTouched(fn: any): void { 169 registerOnTouched(fn: any): void {
139 } 170 }
140 171
141 - setDisabledState?(isDisabled: boolean): void { 172 + setDisabledState(isDisabled: boolean): void {
142 this.disabled = isDisabled; 173 this.disabled = isDisabled;
143 if (this.disabled) { 174 if (this.disabled) {
144 this.filterPredicateValueFormGroup.disable({emitEvent: false}); 175 this.filterPredicateValueFormGroup.disable({emitEvent: false});
@@ -147,28 +178,35 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @@ -147,28 +178,35 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
147 } 178 }
148 } 179 }
149 180
  181 + validate(): ValidationErrors | null {
  182 + return this.filterPredicateValueFormGroup.valid ? null : {
  183 + filterPredicateValue: {valid: false}
  184 + };
  185 + }
  186 +
150 writeValue(predicateValue: FilterPredicateValue<string | number | boolean>): void { 187 writeValue(predicateValue: FilterPredicateValue<string | number | boolean>): void {
  188 + this.propagateChangePending = false;
151 this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false}); 189 this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false});
152 - this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').patchValue(predicateValue.dynamicValue ?  
153 - predicateValue.dynamicValue.sourceType : null, {emitEvent: false});  
154 - this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').patchValue(predicateValue.dynamicValue ?  
155 - predicateValue.dynamicValue.sourceAttribute : null, {emitEvent: false});  
156 - this.filterPredicateValueFormGroup.get('dynamicValue.inherit').patchValue(predicateValue.dynamicValue ?  
157 - predicateValue.dynamicValue.inherit : false, {emitEvent: false}); 190 + this.filterPredicateValueFormGroup.get('dynamicValue').patchValue({
  191 + sourceType: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceType : null,
  192 + sourceAttribute: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceAttribute : null,
  193 + inherit: predicateValue.dynamicValue ? predicateValue.dynamicValue.inherit : false
  194 + }, {emitEvent: this.onlyUserDynamicSource});
158 this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType); 195 this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType);
159 } 196 }
160 197
161 private updateModel() { 198 private updateModel() {
162 - let predicateValue: FilterPredicateValue<string | number | boolean> = null;  
163 - if (this.filterPredicateValueFormGroup.valid) {  
164 - predicateValue = this.filterPredicateValueFormGroup.getRawValue();  
165 - if (predicateValue.dynamicValue) {  
166 - if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) {  
167 - predicateValue.dynamicValue = null;  
168 - } 199 + const predicateValue: FilterPredicateValue<string | number | boolean> = this.filterPredicateValueFormGroup.getRawValue();
  200 + if (predicateValue.dynamicValue) {
  201 + if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) {
  202 + predicateValue.dynamicValue = null;
169 } 203 }
170 } 204 }
171 - this.propagateChange(predicateValue); 205 + if (this.propagateChange) {
  206 + this.propagateChange(predicateValue);
  207 + } else {
  208 + this.propagateChangePending = true;
  209 + }
172 } 210 }
173 211
174 private updateShowInheritMode(sourceType: DynamicValueSourceType) { 212 private updateShowInheritMode(sourceType: DynamicValueSourceType) {
@@ -179,4 +217,16 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @@ -179,4 +217,16 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
179 this.inheritMode = false; 217 this.inheritMode = false;
180 } 218 }
181 } 219 }
  220 +
  221 + private updateValidationDynamicMode() {
  222 + if (this.onlyUserDynamicSource) {
  223 + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').setValidators(Validators.required);
  224 + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').setValidators(Validators.required);
  225 + } else {
  226 + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').clearValidators();
  227 + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').clearValidators();
  228 + }
  229 + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').updateValueAndValidity({emitEvent: false});
  230 + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').updateValueAndValidity({emitEvent: false});
  231 + }
182 } 232 }
@@ -15,11 +15,17 @@ @@ -15,11 +15,17 @@
15 /// 15 ///
16 16
17 import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';  
19 import { 18 import {
20 - EntityKeyValueType,  
21 - FilterPredicateType, KeyFilterPredicate, KeyFilterPredicateInfo  
22 -} from '@shared/models/query/query.models'; 19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
  28 +import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models';
23 29
24 @Component({ 30 @Component({
25 selector: 'tb-filter-predicate', 31 selector: 'tb-filter-predicate',
@@ -30,10 +36,15 @@ import { @@ -30,10 +36,15 @@ import {
30 provide: NG_VALUE_ACCESSOR, 36 provide: NG_VALUE_ACCESSOR,
31 useExisting: forwardRef(() => FilterPredicateComponent), 37 useExisting: forwardRef(() => FilterPredicateComponent),
32 multi: true 38 multi: true
  39 + },
  40 + {
  41 + provide: NG_VALIDATORS,
  42 + useExisting: forwardRef(() => FilterPredicateComponent),
  43 + multi: true
33 } 44 }
34 ] 45 ]
35 }) 46 })
36 -export class FilterPredicateComponent implements ControlValueAccessor, OnInit { 47 +export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
37 48
38 @Input() disabled: boolean; 49 @Input() disabled: boolean;
39 50
@@ -75,7 +86,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit { @@ -75,7 +86,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
75 registerOnTouched(fn: any): void { 86 registerOnTouched(fn: any): void {
76 } 87 }
77 88
78 - setDisabledState?(isDisabled: boolean): void { 89 + setDisabledState(isDisabled: boolean): void {
79 this.disabled = isDisabled; 90 this.disabled = isDisabled;
80 if (this.disabled) { 91 if (this.disabled) {
81 this.filterPredicateFormGroup.disable({emitEvent: false}); 92 this.filterPredicateFormGroup.disable({emitEvent: false});
@@ -84,6 +95,12 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit { @@ -84,6 +95,12 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
84 } 95 }
85 } 96 }
86 97
  98 + validate(): ValidationErrors | null {
  99 + return this.filterPredicateFormGroup.valid ? null : {
  100 + filterPredicate: {valid: false}
  101 + };
  102 + }
  103 +
87 writeValue(predicate: KeyFilterPredicateInfo): void { 104 writeValue(predicate: KeyFilterPredicateInfo): void {
88 this.type = predicate.keyFilterPredicate.type; 105 this.type = predicate.keyFilterPredicate.type;
89 this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); 106 this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false});
@@ -22,7 +22,10 @@ import { @@ -22,7 +22,10 @@ import {
22 FormBuilder, 22 FormBuilder,
23 FormControl, 23 FormControl,
24 FormGroup, 24 FormGroup,
  25 + NG_VALIDATORS,
25 NG_VALUE_ACCESSOR, 26 NG_VALUE_ACCESSOR,
  27 + ValidationErrors,
  28 + Validator,
26 Validators 29 Validators
27 } from '@angular/forms'; 30 } from '@angular/forms';
28 import { Observable, Subscription } from 'rxjs'; 31 import { Observable, Subscription } from 'rxjs';
@@ -46,10 +49,15 @@ import { EntityId } from '@shared/models/id/entity-id'; @@ -46,10 +49,15 @@ import { EntityId } from '@shared/models/id/entity-id';
46 provide: NG_VALUE_ACCESSOR, 49 provide: NG_VALUE_ACCESSOR,
47 useExisting: forwardRef(() => KeyFilterListComponent), 50 useExisting: forwardRef(() => KeyFilterListComponent),
48 multi: true 51 multi: true
  52 + },
  53 + {
  54 + provide: NG_VALIDATORS,
  55 + useExisting: forwardRef(() => KeyFilterListComponent),
  56 + multi: true
49 } 57 }
50 ] 58 ]
51 }) 59 })
52 -export class KeyFilterListComponent implements ControlValueAccessor, OnInit { 60 +export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit {
53 61
54 @Input() disabled: boolean; 62 @Input() disabled: boolean;
55 63
@@ -104,6 +112,12 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -104,6 +112,12 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
104 } 112 }
105 } 113 }
106 114
  115 + validate(): ValidationErrors | null {
  116 + return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : {
  117 + keyFilterList: {valid: false}
  118 + };
  119 + }
  120 +
107 writeValue(keyFilters: Array<KeyFilterInfo>): void { 121 writeValue(keyFilters: Array<KeyFilterInfo>): void {
108 if (this.valueChangeSubscription) { 122 if (this.valueChangeSubscription) {
109 this.valueChangeSubscription.unsubscribe(); 123 this.valueChangeSubscription.unsubscribe();
@@ -15,7 +15,16 @@ @@ -15,7 +15,16 @@
15 /// 15 ///
16 16
17 import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; 18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
19 import { 28 import {
20 EntityKeyValueType, 29 EntityKeyValueType,
21 FilterPredicateType, 30 FilterPredicateType,
@@ -33,10 +42,15 @@ import { @@ -33,10 +42,15 @@ import {
33 provide: NG_VALUE_ACCESSOR, 42 provide: NG_VALUE_ACCESSOR,
34 useExisting: forwardRef(() => NumericFilterPredicateComponent), 43 useExisting: forwardRef(() => NumericFilterPredicateComponent),
35 multi: true 44 multi: true
  45 + },
  46 + {
  47 + provide: NG_VALIDATORS,
  48 + useExisting: forwardRef(() => NumericFilterPredicateComponent),
  49 + multi: true
36 } 50 }
37 ] 51 ]
38 }) 52 })
39 -export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit { 53 +export class NumericFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
40 54
41 @Input() disabled: boolean; 55 @Input() disabled: boolean;
42 56
@@ -76,7 +90,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On @@ -76,7 +90,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
76 registerOnTouched(fn: any): void { 90 registerOnTouched(fn: any): void {
77 } 91 }
78 92
79 - setDisabledState?(isDisabled: boolean): void { 93 + setDisabledState(isDisabled: boolean): void {
80 this.disabled = isDisabled; 94 this.disabled = isDisabled;
81 if (this.disabled) { 95 if (this.disabled) {
82 this.numericFilterPredicateFormGroup.disable({emitEvent: false}); 96 this.numericFilterPredicateFormGroup.disable({emitEvent: false});
@@ -85,17 +99,20 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On @@ -85,17 +99,20 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
85 } 99 }
86 } 100 }
87 101
  102 + validate(): ValidationErrors | null {
  103 + return this.numericFilterPredicateFormGroup.valid ? null : {
  104 + numericFilterPredicate: {valid: false}
  105 + };
  106 + }
  107 +
88 writeValue(predicate: NumericFilterPredicate): void { 108 writeValue(predicate: NumericFilterPredicate): void {
89 this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); 109 this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
90 this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); 110 this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
91 } 111 }
92 112
93 private updateModel() { 113 private updateModel() {
94 - let predicate: NumericFilterPredicate = null;  
95 - if (this.numericFilterPredicateFormGroup.valid) {  
96 - predicate = this.numericFilterPredicateFormGroup.getRawValue();  
97 - predicate.type = FilterPredicateType.NUMERIC;  
98 - } 114 + const predicate: NumericFilterPredicate = this.numericFilterPredicateFormGroup.getRawValue();
  115 + predicate.type = FilterPredicateType.NUMERIC;
99 this.propagateChange(predicate); 116 this.propagateChange(predicate);
100 } 117 }
101 118
@@ -15,7 +15,16 @@ @@ -15,7 +15,16 @@
15 /// 15 ///
16 16
17 import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; 18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
19 import { 28 import {
20 EntityKeyValueType, 29 EntityKeyValueType,
21 FilterPredicateType, 30 FilterPredicateType,
@@ -33,10 +42,15 @@ import { @@ -33,10 +42,15 @@ import {
33 provide: NG_VALUE_ACCESSOR, 42 provide: NG_VALUE_ACCESSOR,
34 useExisting: forwardRef(() => StringFilterPredicateComponent), 43 useExisting: forwardRef(() => StringFilterPredicateComponent),
35 multi: true 44 multi: true
  45 + },
  46 + {
  47 + provide: NG_VALIDATORS,
  48 + useExisting: forwardRef(() => StringFilterPredicateComponent),
  49 + multi: true
36 } 50 }
37 ] 51 ]
38 }) 52 })
39 -export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit { 53 +export class StringFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
40 54
41 @Input() disabled: boolean; 55 @Input() disabled: boolean;
42 56
@@ -90,12 +104,15 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI @@ -90,12 +104,15 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
90 this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false}); 104 this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false});
91 } 105 }
92 106
  107 + validate(c): ValidationErrors {
  108 + return this.stringFilterPredicateFormGroup.valid ? null : {
  109 + stringFilterPredicate: {valid: false}
  110 + };
  111 + }
  112 +
93 private updateModel() { 113 private updateModel() {
94 - let predicate: StringFilterPredicate = null;  
95 - if (this.stringFilterPredicateFormGroup.valid) {  
96 - predicate = this.stringFilterPredicateFormGroup.getRawValue();  
97 - predicate.type = FilterPredicateType.STRING;  
98 - } 114 + const predicate: StringFilterPredicate = this.stringFilterPredicateFormGroup.getRawValue();
  115 + predicate.type = FilterPredicateType.STRING;
99 this.propagateChange(predicate); 116 this.propagateChange(predicate);
100 } 117 }
101 118
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<div class="tb-json-input" tb-toast toastTarget="{{ toastTargetId }}">
  20 + <form *ngIf="attributeUpdateFormGroup"
  21 + fxLayout="column"
  22 + class="tb-json-input__form"
  23 + [formGroup]="attributeUpdateFormGroup"
  24 + (ngSubmit)="save()">
  25 + <div fxLayout="column" fxLayoutGap="10px" fxFlex *ngIf="entityDetected && isValidParameter && dataKeyDetected">
  26 + <fieldset fxFlex>
  27 + <tb-json-object-edit
  28 + [editorStyle]="{minHeight: '100px'}"
  29 + fillHeight="true"
  30 + [required]="settings.attributeRequired"
  31 + label="{{ settings.showLabel ? labelValue : '' }}"
  32 + formControlName="currentValue"
  33 + (focusin)="isFocused = true;"
  34 + (focusout)="isFocused = false;"
  35 + ></tb-json-object-edit>
  36 + </fieldset>
  37 + <div class="tb-json-input-form__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
  38 + <button mat-button color="primary"
  39 + type="button"
  40 + [disabled]="!attributeUpdateFormGroup.dirty"
  41 + (click)="discard()"
  42 + matTooltip="{{ 'widgets.input-widgets.discard-changes' | translate }}"
  43 + matTooltipPosition="above">
  44 + {{ "action.undo" | translate }}
  45 + </button>
  46 + <button mat-button mat-raised-button color="primary"
  47 + type="submit"
  48 + [disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty">
  49 + {{ "action.save" | translate }}
  50 + </button>
  51 + </div>
  52 + </div>
  53 +
  54 + <div fxLayout="column" fxLayoutAlign="center center" fxFlex *ngIf="!entityDetected || !dataKeyDetected || !isValidParameter">
  55 + <div class="tb-json-input__error"
  56 + *ngIf="!entityDetected">
  57 + {{ 'widgets.input-widgets.no-entity-selected' | translate }}
  58 + </div>
  59 + <div class="tb-json-input__error"
  60 + *ngIf="entityDetected && !dataKeyDetected">
  61 + {{ 'widgets.input-widgets.no-datakey-selected' | translate }}
  62 + </div>
  63 + <div class="tb-json-input__error"
  64 + *ngIf="dataKeyDetected && !isValidParameter">
  65 + {{ errorMessage | translate }}
  66 + </div>
  67 + </div>
  68 + </form>
  69 +</div>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-json-input {
  18 + width: 100%;
  19 + height: 100%;
  20 + padding: 5px;
  21 +
  22 + &__form {
  23 + overflow: auto;
  24 + height: 100%;
  25 + }
  26 +
  27 + &__error {
  28 + text-align: center;
  29 + font-size: 18px;
  30 + color: #a0a0a0;
  31 + }
  32 +}
  33 +
  34 +.tb-toast {
  35 + font-size: 14px!important;
  36 +}
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Input, OnInit } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { WidgetContext } from '@home/models/widget-component.models';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { UtilsService } from '@core/services/utils.service';
  23 +import { TranslateService } from '@ngx-translate/core';
  24 +import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
  25 +import { IWidgetSubscription } from '@core/api/widget-api.models';
  26 +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
  27 +import { AttributeService } from '@core/http/attribute.service';
  28 +import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
  29 +import { EntityId } from '@shared/models/id/entity-id';
  30 +import { EntityType } from '@shared/models/entity-type.models';
  31 +import { createLabelFromDatasource } from '@core/utils';
  32 +import { Observable } from 'rxjs';
  33 +
  34 +enum JsonInputWidgetMode {
  35 + ATTRIBUTE = 'ATTRIBUTE',
  36 + TIME_SERIES = 'TIME_SERIES',
  37 +}
  38 +
  39 +interface JsonInputWidgetSettings {
  40 + widgetTitle: string;
  41 + widgetMode: JsonInputWidgetMode;
  42 + attributeScope?: AttributeScope;
  43 + showLabel: boolean;
  44 + labelValue?: string;
  45 + attributeRequired: boolean;
  46 + showResultMessage: boolean;
  47 +}
  48 +
  49 +@Component({
  50 + selector: 'tb-json-input-widget ',
  51 + templateUrl: './json-input-widget.component.html',
  52 + styleUrls: ['./json-input-widget.component.scss']
  53 +})
  54 +export class JsonInputWidgetComponent extends PageComponent implements OnInit {
  55 +
  56 + @Input()
  57 + ctx: WidgetContext;
  58 +
  59 + public settings: JsonInputWidgetSettings;
  60 + private widgetConfig: WidgetConfig;
  61 + private subscription: IWidgetSubscription;
  62 + private datasource: Datasource;
  63 +
  64 + labelValue: string;
  65 +
  66 + entityDetected = false;
  67 + dataKeyDetected = false;
  68 + isValidParameter = false;
  69 + errorMessage: string;
  70 +
  71 + isFocused: boolean;
  72 + originalValue: any;
  73 + attributeUpdateFormGroup: FormGroup;
  74 +
  75 + toastTargetId = 'json-input-widget' + this.utils.guid();
  76 +
  77 + constructor(protected store: Store<AppState>,
  78 + private utils: UtilsService,
  79 + private fb: FormBuilder,
  80 + private attributeService: AttributeService,
  81 + private translate: TranslateService) {
  82 + super(store);
  83 + }
  84 +
  85 + ngOnInit(): void {
  86 + this.ctx.$scope.jsonInputWidget = this;
  87 + this.settings = this.ctx.settings;
  88 + this.widgetConfig = this.ctx.widgetConfig;
  89 + this.subscription = this.ctx.defaultSubscription;
  90 + this.datasource = this.subscription.datasources[0];
  91 + this.initializeConfig();
  92 + this.validateDatasources();
  93 + this.buildForm();
  94 + this.ctx.updateWidgetParams();
  95 + }
  96 +
  97 + private initializeConfig() {
  98 + if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
  99 + const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle);
  100 + this.ctx.widgetTitle = this.utils.customTranslation(title, title);
  101 + } else {
  102 + this.ctx.widgetTitle = this.ctx.widgetConfig.title;
  103 + }
  104 +
  105 + if (this.settings.labelValue && this.settings.labelValue.length) {
  106 + const label = createLabelFromDatasource(this.datasource, this.settings.labelValue);
  107 + this.labelValue = this.utils.customTranslation(label, label);
  108 + } else {
  109 + this.labelValue = this.translate.instant('widgets.input-widgets.value');
  110 + }
  111 + }
  112 +
  113 + private validateDatasources() {
  114 + if (this.datasource?.type === DatasourceType.entity) {
  115 + this.entityDetected = true;
  116 + if (this.datasource.dataKeys.length) {
  117 + this.dataKeyDetected = true;
  118 +
  119 + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
  120 + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
  121 + if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) {
  122 + this.isValidParameter = true;
  123 + } else {
  124 + this.errorMessage = 'widgets.input-widgets.not-allowed-entity';
  125 + }
  126 + } else {
  127 + this.errorMessage = 'widgets.input-widgets.no-attribute-selected';
  128 + }
  129 + } else {
  130 + if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
  131 + this.isValidParameter = true;
  132 + } else {
  133 + this.errorMessage = 'widgets.input-widgets.no-timeseries-selected';
  134 + }
  135 + }
  136 +
  137 + }
  138 + }
  139 + }
  140 +
  141 + private buildForm() {
  142 + const validators: ValidatorFn[] = [];
  143 + if (this.settings.attributeRequired) {
  144 + validators.push(Validators.required);
  145 + }
  146 + this.attributeUpdateFormGroup = this.fb.group({
  147 + currentValue: [{}, validators]
  148 + });
  149 + this.attributeUpdateFormGroup.valueChanges.subscribe( () => {
  150 + this.ctx.detectChanges();
  151 + });
  152 + }
  153 +
  154 + private updateWidgetData(data: Array<DatasourceData>) {
  155 + if (this.isValidParameter) {
  156 + let value = {};
  157 + if (data[0].data[0][1] !== '') {
  158 + try {
  159 + value = JSON.parse(data[0].data[0][1]);
  160 + } catch (e) {
  161 + value = data[0].data[0][1];
  162 + }
  163 + }
  164 + this.originalValue = value;
  165 + if (!this.isFocused) {
  166 + this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue);
  167 + this.ctx.detectChanges();
  168 + }
  169 + }
  170 + }
  171 +
  172 + public onDataUpdated() {
  173 + this.updateWidgetData(this.subscription.data);
  174 + }
  175 +
  176 + public save() {
  177 + this.isFocused = false;
  178 +
  179 + const attributeToSave: AttributeData = {
  180 + key: this.datasource.dataKeys[0].name,
  181 + value: this.attributeUpdateFormGroup.get('currentValue').value
  182 + };
  183 +
  184 + const entityId: EntityId = {
  185 + entityType: this.datasource.entityType,
  186 + id: this.datasource.entityId
  187 + };
  188 +
  189 + let saveAttributeObservable: Observable<any>;
  190 + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
  191 + saveAttributeObservable = this.attributeService.saveEntityAttributes(
  192 + entityId,
  193 + this.settings.attributeScope,
  194 + [ attributeToSave ],
  195 + {}
  196 + );
  197 + } else {
  198 + saveAttributeObservable = this.attributeService.saveEntityTimeseries(
  199 + entityId,
  200 + LatestTelemetry.LATEST_TELEMETRY,
  201 + [ attributeToSave ],
  202 + {}
  203 + );
  204 + }
  205 + saveAttributeObservable.subscribe(
  206 + () => {
  207 + this.attributeUpdateFormGroup.markAsPristine();
  208 + this.ctx.detectChanges();
  209 + if (this.settings.showResultMessage) {
  210 + this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'),
  211 + 1000, 'bottom', 'left', this.toastTargetId);
  212 + }
  213 + },
  214 + () => {
  215 + if (this.settings.showResultMessage) {
  216 + this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'),
  217 + 'bottom', 'left', this.toastTargetId);
  218 + }
  219 + });
  220 + }
  221 +
  222 + public discard() {
  223 + this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false});
  224 + this.attributeUpdateFormGroup.markAsPristine();
  225 + this.isFocused = false;
  226 + }
  227 +}
@@ -19,7 +19,8 @@ import { @@ -19,7 +19,8 @@ import {
19 createLabelFromDatasource, 19 createLabelFromDatasource,
20 hashCode, 20 hashCode,
21 isDefined, 21 isDefined,
22 - isDefinedAndNotNull, isFunction, 22 + isDefinedAndNotNull,
  23 + isFunction,
23 isNumber, 24 isNumber,
24 isUndefined, 25 isUndefined,
25 padValue 26 padValue
@@ -30,7 +31,7 @@ import { Datasource, DatasourceData } from '@shared/models/widget.models'; @@ -30,7 +31,7 @@ import { Datasource, DatasourceData } from '@shared/models/widget.models';
30 import _ from 'lodash'; 31 import _ from 'lodash';
31 import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes'; 32 import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes';
32 import { addCondition, mergeSchemes } from '@core/schema-utils'; 33 import { addCondition, mergeSchemes } from '@core/schema-utils';
33 -import L, {Projection} from "leaflet"; 34 +import L from 'leaflet';
34 35
35 export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) { 36 export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) {
36 const providerSchema = _.cloneDeep(mapProviderSchema); 37 const providerSchema = _.cloneDeep(mapProviderSchema);
@@ -318,22 +319,24 @@ export const parseWithTranslation = { @@ -318,22 +319,24 @@ export const parseWithTranslation = {
318 }; 319 };
319 320
320 export function parseData(input: DatasourceData[]): FormattedData[] { 321 export function parseData(input: DatasourceData[]): FormattedData[] {
321 - return _(input).groupBy(el => el?.datasource?.entityName) 322 + return _(input).groupBy(el => el?.datasource.entityId + el?.datasource.entityType)
322 .values().value().map((entityArray, i) => { 323 .values().value().map((entityArray, i) => {
323 const obj: FormattedData = { 324 const obj: FormattedData = {
324 entityName: entityArray[0]?.datasource?.entityName, 325 entityName: entityArray[0]?.datasource?.entityName,
325 - entityId: entityArray[0]?.datasource?.entityId,  
326 - entityType: entityArray[0]?.datasource?.entityType,  
327 - $datasource: entityArray[0]?.datasource, 326 + entityId: entityArray[0].datasource.entityId,
  327 + entityType: entityArray[0].datasource.entityType,
  328 + $datasource: entityArray[0].datasource,
328 dsIndex: i, 329 dsIndex: i,
329 deviceType: null 330 deviceType: null
330 }; 331 };
331 entityArray.filter(el => el.data.length).forEach(el => { 332 entityArray.filter(el => el.data.length).forEach(el => {
332 - const indexDate = el?.data?.length ? el.data.length - 1 : 0;  
333 - obj[el?.dataKey?.label] = el?.data[indexDate][1];  
334 - obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0];  
335 - if (el?.dataKey?.label === 'type') {  
336 - obj.deviceType = el?.data[indexDate][1]; 333 + const indexDate = el.data.length ? el.data.length - 1 : 0;
  334 + if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') {
  335 + obj[el.dataKey.label] = el.data[indexDate][1];
  336 + obj[el.dataKey.label + '|ts'] = el.data[indexDate][0];
  337 + if (el.dataKey.label === 'type') {
  338 + obj.deviceType = el.data[indexDate][1];
  339 + }
337 } 340 }
338 }); 341 });
339 return obj; 342 return obj;
@@ -121,7 +121,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy @@ -121,7 +121,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
121 121
122 this.switchResize$ = new ResizeObserver(() => { 122 this.switchResize$ = new ResizeObserver(() => {
123 this.resize(); 123 this.resize();
124 - }) 124 + });
125 this.switchResize$.observe(this.switchContainerRef.nativeElement); 125 this.switchResize$.observe(this.switchContainerRef.nativeElement);
126 this.init(); 126 this.init();
127 } 127 }
@@ -202,13 +202,13 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy @@ -202,13 +202,13 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
202 let width = this.switchContainer.width(); 202 let width = this.switchContainer.width();
203 let height = this.switchContainer.height(); 203 let height = this.switchContainer.height();
204 if (this.showOnOffLabels) { 204 if (this.showOnOffLabels) {
205 - height = height*2/3; 205 + height = height * 2 / 3;
206 } 206 }
207 - const ratio = width/height; 207 + const ratio = width / height;
208 if (ratio > switchAspectRation) { 208 if (ratio > switchAspectRation) {
209 - width = height*switchAspectRation; 209 + width = height * switchAspectRation;
210 } else { 210 } else {
211 - height = width/switchAspectRation; 211 + height = width / switchAspectRation;
212 } 212 }
213 this.switchElement.css({width, height}); 213 this.switchElement.css({width, height});
214 this.matSlideToggle.css({width, height, minWidth: width}); 214 this.matSlideToggle.css({width, height, minWidth: width});
@@ -232,11 +232,11 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy @@ -232,11 +232,11 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
232 fontSize--; 232 fontSize--;
233 textWidth = this.measureTextWidth(text, fontSize); 233 textWidth = this.measureTextWidth(text, fontSize);
234 } 234 }
235 - element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); 235 + element.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'});
236 } 236 }
237 237
238 private measureTextWidth(text: string, fontSize: number): number { 238 private measureTextWidth(text: string, fontSize: number): number {
239 - this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); 239 + this.textMeasure.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'});
240 this.textMeasure.text(text); 240 this.textMeasure.text(text);
241 return this.textMeasure.width(); 241 return this.textMeasure.width();
242 } 242 }
@@ -260,6 +260,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy @@ -260,6 +260,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
260 this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe( 260 this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe(
261 (responseBody) => { 261 (responseBody) => {
262 this.setValue(this.parseValueFunction(responseBody)); 262 this.setValue(this.parseValueFunction(responseBody));
  263 + this.ctx.detectChanges();
263 }, 264 },
264 () => { 265 () => {
265 const errorText = this.ctx.defaultSubscription.rpcErrorText; 266 const errorText = this.ctx.defaultSubscription.rpcErrorText;
@@ -38,6 +38,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor @@ -38,6 +38,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
38 import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; 38 import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
39 import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; 39 import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
40 import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; 40 import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
  41 +import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component';
41 42
42 @NgModule({ 43 @NgModule({
43 declarations: 44 declarations:
@@ -51,6 +52,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges- @@ -51,6 +52,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
51 EdgesOverviewWidgetComponent, 52 EdgesOverviewWidgetComponent,
52 DateRangeNavigatorWidgetComponent, 53 DateRangeNavigatorWidgetComponent,
53 DateRangeNavigatorPanelComponent, 54 DateRangeNavigatorPanelComponent,
  55 + JsonInputWidgetComponent,
54 MultipleInputWidgetComponent, 56 MultipleInputWidgetComponent,
55 TripAnimationComponent, 57 TripAnimationComponent,
56 PhotoCameraInputWidgetComponent, 58 PhotoCameraInputWidgetComponent,
@@ -72,6 +74,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges- @@ -72,6 +74,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
72 EdgesOverviewWidgetComponent, 74 EdgesOverviewWidgetComponent,
73 RpcWidgetsModule, 75 RpcWidgetsModule,
74 DateRangeNavigatorWidgetComponent, 76 DateRangeNavigatorWidgetComponent,
  77 + JsonInputWidgetComponent,
75 MultipleInputWidgetComponent, 78 MultipleInputWidgetComponent,
76 TripAnimationComponent, 79 TripAnimationComponent,
77 PhotoCameraInputWidgetComponent, 80 PhotoCameraInputWidgetComponent,
@@ -104,7 +104,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse @@ -104,7 +104,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
104 )); 104 ));
105 }; 105 };
106 this.config.onEntityAction = action => this.onAssetAction(action); 106 this.config.onEntityAction = action => this.onAssetAction(action);
107 - this.config.detailsReadonly = () => this.config.componentsData.assetScope === 'customer_user'; 107 + this.config.detailsReadonly = () => (this.config.componentsData.assetScope === 'customer_user' || this.config.componentsData.assetScope === 'edge_customer_user');
108 108
109 this.config.headerComponent = AssetTableHeaderComponent; 109 this.config.headerComponent = AssetTableHeaderComponent;
110 110
@@ -151,7 +151,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse @@ -151,7 +151,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
151 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope); 151 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope);
152 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope); 152 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope);
153 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.assetScope); 153 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.assetScope);
154 - this.config.addEnabled = this.config.componentsData.assetScope !== 'customer_user'; 154 + this.config.addEnabled = !(this.config.componentsData.assetScope === 'customer_user' || this.config.componentsData.assetScope === 'edge_customer_user');
155 this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant'; 155 this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant';
156 this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant'; 156 this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant';
157 return this.config; 157 return this.config;
@@ -103,7 +103,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< @@ -103,7 +103,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
103 return this.dashboardService.saveDashboard(dashboard as Dashboard); 103 return this.dashboardService.saveDashboard(dashboard as Dashboard);
104 }; 104 };
105 this.config.onEntityAction = action => this.onDashboardAction(action); 105 this.config.onEntityAction = action => this.onDashboardAction(action);
106 - this.config.detailsReadonly = () => this.config.componentsData.dashboardScope === 'customer_user'; 106 + this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user');
107 } 107 }
108 108
109 resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DashboardInfo | Dashboard>> { 109 resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DashboardInfo | Dashboard>> {
@@ -147,7 +147,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< @@ -147,7 +147,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
147 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope); 147 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope);
148 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope); 148 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope);
149 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.dashboardScope); 149 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.dashboardScope);
150 - this.config.addEnabled = this.config.componentsData.dashboardScope !== 'customer_user'; 150 + this.config.addEnabled = !(this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user');
151 this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant'; 151 this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant';
152 this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant'; 152 this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant';
153 return this.config; 153 return this.config;
@@ -112,7 +112,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev @@ -112,7 +112,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
112 )); 112 ));
113 }; 113 };
114 this.config.onEntityAction = action => this.onDeviceAction(action); 114 this.config.onEntityAction = action => this.onDeviceAction(action);
115 - this.config.detailsReadonly = () => this.config.componentsData.deviceScope === 'customer_user'; 115 + this.config.detailsReadonly = () => (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
116 116
117 this.config.headerComponent = DeviceTableHeaderComponent; 117 this.config.headerComponent = DeviceTableHeaderComponent;
118 118
@@ -161,7 +161,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev @@ -161,7 +161,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
161 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); 161 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
162 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope); 162 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
163 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope); 163 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
164 - this.config.addEnabled = this.config.componentsData.deviceScope !== 'customer_user'; 164 + this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
165 this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant'; 165 this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
166 this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant'; 166 this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
167 return this.config; 167 return this.config;
@@ -59,21 +59,6 @@ const routes: Routes = [ @@ -59,21 +59,6 @@ const routes: Routes = [
59 } 59 }
60 }, 60 },
61 { 61 {
62 - path: ':edgeId/ruleChains',  
63 - component: EntitiesTableComponent,  
64 - data: {  
65 - auth: [Authority.TENANT_ADMIN],  
66 - ruleChainsType: 'edge',  
67 - breadcrumb: {  
68 - label: 'edge.edge-rulechains',  
69 - icon: 'settings_ethernet'  
70 - },  
71 - },  
72 - resolve: {  
73 - entitiesTableConfig: RuleChainsTableConfigResolver  
74 - }  
75 - },  
76 - {  
77 path: ':edgeId/assets', 62 path: ':edgeId/assets',
78 component: EntitiesTableComponent, 63 component: EntitiesTableComponent,
79 data: { 64 data: {
@@ -157,6 +142,50 @@ const routes: Routes = [ @@ -157,6 +142,50 @@ const routes: Routes = [
157 ] 142 ]
158 }, 143 },
159 { 144 {
  145 + path: ':edgeId/ruleChains',
  146 + data: {
  147 + breadcrumb: {
  148 + label: 'edge.edge-rulechains',
  149 + icon: 'settings_ethernet'
  150 + }
  151 + },
  152 + children: [
  153 + {
  154 + path: '',
  155 + component: EntitiesTableComponent,
  156 + data: {
  157 + auth: [Authority.TENANT_ADMIN],
  158 + title: 'edge.rulechains',
  159 + ruleChainsType: 'edge'
  160 + },
  161 + resolve: {
  162 + entitiesTableConfig: RuleChainsTableConfigResolver
  163 + }
  164 + },
  165 + {
  166 + path: ':ruleChainId',
  167 + component: RuleChainPageComponent,
  168 + canDeactivate: [ConfirmOnExitGuard],
  169 + data: {
  170 + breadcrumb: {
  171 + labelFunction: ruleChainBreadcumbLabelFunction,
  172 + icon: 'settings_ethernet'
  173 + } as BreadCrumbConfig<RuleChainPageComponent>,
  174 + auth: [Authority.TENANT_ADMIN],
  175 + title: 'rulechain.edge-rulechain',
  176 + import: false,
  177 + ruleChainType: RuleChainType.EDGE
  178 + },
  179 + resolve: {
  180 + ruleChain: RuleChainResolver,
  181 + ruleChainMetaData: ResolvedRuleChainMetaDataResolver,
  182 + ruleNodeComponents: RuleNodeComponentsResolver,
  183 + tooltipster: TooltipsterResolver
  184 + }
  185 + }
  186 + ]
  187 + },
  188 + {
160 path: 'ruleChains', 189 path: 'ruleChains',
161 data: { 190 data: {
162 breadcrumb: { 191 breadcrumb: {
@@ -44,31 +44,31 @@ @@ -44,31 +44,31 @@
44 <button mat-raised-button color="primary" 44 <button mat-raised-button color="primary"
45 [disabled]="(isLoading$ | async)" 45 [disabled]="(isLoading$ | async)"
46 (click)="onEntityAction($event, 'openEdgeAssets')" 46 (click)="onEntityAction($event, 'openEdgeAssets')"
47 - [fxShow]="!isEdit"> 47 + [fxShow]="!isEdit && edgeScope !== 'customer'">
48 {{'edge.manage-edge-assets' | translate }} 48 {{'edge.manage-edge-assets' | translate }}
49 </button> 49 </button>
50 <button mat-raised-button color="primary" 50 <button mat-raised-button color="primary"
51 [disabled]="(isLoading$ | async)" 51 [disabled]="(isLoading$ | async)"
52 (click)="onEntityAction($event, 'openEdgeDevices')" 52 (click)="onEntityAction($event, 'openEdgeDevices')"
53 - [fxShow]="!isEdit"> 53 + [fxShow]="!isEdit && edgeScope !== 'customer'">
54 {{'edge.manage-edge-devices' | translate }} 54 {{'edge.manage-edge-devices' | translate }}
55 </button> 55 </button>
56 <button mat-raised-button color="primary" 56 <button mat-raised-button color="primary"
57 [disabled]="(isLoading$ | async)" 57 [disabled]="(isLoading$ | async)"
58 (click)="onEntityAction($event, 'openEdgeEntityViews')" 58 (click)="onEntityAction($event, 'openEdgeEntityViews')"
59 - [fxShow]="!isEdit"> 59 + [fxShow]="!isEdit && edgeScope !== 'customer'">
60 {{'edge.manage-edge-entity-views' | translate }} 60 {{'edge.manage-edge-entity-views' | translate }}
61 </button> 61 </button>
62 <button mat-raised-button color="primary" 62 <button mat-raised-button color="primary"
63 [disabled]="(isLoading$ | async)" 63 [disabled]="(isLoading$ | async)"
64 (click)="onEntityAction($event, 'openEdgeDashboards')" 64 (click)="onEntityAction($event, 'openEdgeDashboards')"
65 - [fxShow]="!isEdit"> 65 + [fxShow]="!isEdit && edgeScope !== 'customer'">
66 {{'edge.manage-edge-dashboards' | translate }} 66 {{'edge.manage-edge-dashboards' | translate }}
67 </button> 67 </button>
68 <button mat-raised-button color="primary" 68 <button mat-raised-button color="primary"
69 [disabled]="(isLoading$ | async)" 69 [disabled]="(isLoading$ | async)"
70 (click)="onEntityAction($event, 'openEdgeRuleChains')" 70 (click)="onEntityAction($event, 'openEdgeRuleChains')"
71 - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> 71 + [fxShow]="!isEdit && edgeScope === 'tenant'">
72 {{'edge.manage-edge-rulechains' | translate }} 72 {{'edge.manage-edge-rulechains' | translate }}
73 </button> 73 </button>
74 </div> 74 </div>
@@ -85,7 +85,7 @@ @@ -85,7 +85,7 @@
85 ngxClipboard 85 ngxClipboard
86 (cbOnSuccess)="onEdgeInfoCopied('key')" 86 (cbOnSuccess)="onEdgeInfoCopied('key')"
87 [cbContent]="entity?.routingKey" 87 [cbContent]="entity?.routingKey"
88 - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> 88 + [fxShow]="!isEdit && edgeScope !== 'customer_user'">
89 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> 89 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
90 <span translate>edge.copy-edge-key</span> 90 <span translate>edge.copy-edge-key</span>
91 </button> 91 </button>
@@ -93,7 +93,7 @@ @@ -93,7 +93,7 @@
93 ngxClipboard 93 ngxClipboard
94 (cbOnSuccess)="onEdgeInfoCopied('secret')" 94 (cbOnSuccess)="onEdgeInfoCopied('secret')"
95 [cbContent]="entity?.secret" 95 [cbContent]="entity?.secret"
96 - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> 96 + [fxShow]="!isEdit && edgeScope !== 'customer_user'">
97 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> 97 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
98 <span translate>edge.copy-edge-secret</span> 98 <span translate>edge.copy-edge-secret</span>
99 </button> 99 </button>
@@ -101,7 +101,7 @@ @@ -101,7 +101,7 @@
101 ngxClipboard 101 ngxClipboard
102 [disabled]="(isLoading$ | async)" 102 [disabled]="(isLoading$ | async)"
103 (click)="onEntityAction($event, 'syncEdge')" 103 (click)="onEntityAction($event, 'syncEdge')"
104 - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> 104 + [fxShow]="!isEdit && edgeScope !== 'customer_user'">
105 <mat-icon svgIcon="mdi:sync"></mat-icon> 105 <mat-icon svgIcon="mdi:sync"></mat-icon>
106 <span translate>edge.sync</span> 106 <span translate>edge.sync</span>
107 </button> 107 </button>
@@ -132,7 +132,7 @@ @@ -132,7 +132,7 @@
132 [required]="true" 132 [required]="true"
133 [entityType]="entityType.EDGE"> 133 [entityType]="entityType.EDGE">
134 </tb-entity-subtype-autocomplete> 134 </tb-entity-subtype-autocomplete>
135 - <div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> 135 + <div [fxShow]="edgeScope !== 'customer_user'">
136 <div class="tb-hint" [innerHTML]="'edge.edge-license-key-hint' | translate"></div> 136 <div class="tb-hint" [innerHTML]="'edge.edge-license-key-hint' | translate"></div>
137 <mat-form-field class="mat-block"> 137 <mat-form-field class="mat-block">
138 <mat-label translate>edge.edge-license-key</mat-label> 138 <mat-label translate>edge.edge-license-key</mat-label>
@@ -142,7 +142,7 @@ @@ -142,7 +142,7 @@
142 </mat-error> 142 </mat-error>
143 </mat-form-field> 143 </mat-form-field>
144 </div> 144 </div>
145 - <div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> 145 + <div [fxShow]="edgeScope !== 'customer_user'">
146 <div translate class="tb-hint">edge.cloud-endpoint-hint</div> 146 <div translate class="tb-hint">edge.cloud-endpoint-hint</div>
147 <mat-form-field class="mat-block"> 147 <mat-form-field class="mat-block">
148 <mat-label translate>edge.cloud-endpoint</mat-label> 148 <mat-label translate>edge.cloud-endpoint</mat-label>
@@ -153,7 +153,7 @@ @@ -153,7 +153,7 @@
153 </mat-form-field> 153 </mat-form-field>
154 </div> 154 </div>
155 </fieldset> 155 </fieldset>
156 - <div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> 156 + <div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
157 <mat-form-field class="mat-block" fxFlex> 157 <mat-form-field class="mat-block" fxFlex>
158 <mat-label translate>edge.edge-key</mat-label> 158 <mat-label translate>edge.edge-key</mat-label>
159 <input matInput formControlName="routingKey"> 159 <input matInput formControlName="routingKey">
@@ -164,7 +164,7 @@ @@ -164,7 +164,7 @@
164 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> 164 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
165 </button> 165 </button>
166 </div> 166 </div>
167 - <div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> 167 + <div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
168 <mat-form-field class="mat-block" fxFlex> 168 <mat-form-field class="mat-block" fxFlex>
169 <mat-label translate>edge.edge-secret</mat-label> 169 <mat-label translate>edge.edge-secret</mat-label>
170 <input matInput formControlName="secret"> 170 <input matInput formControlName="secret">
@@ -477,8 +477,8 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI @@ -477,8 +477,8 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
477 $event.stopPropagation(); 477 $event.stopPropagation();
478 } 478 }
479 this.dialogService.confirm( 479 this.dialogService.confirm(
480 - this.translate.instant('edge.unassign-edge-title', {count: edges.length}),  
481 - this.translate.instant('edge.unassign-edge-text'), 480 + this.translate.instant('edge.unassign-edges-title', {count: edges.length}),
  481 + this.translate.instant('edge.unassign-edges-text'),
482 this.translate.instant('action.no'), 482 this.translate.instant('action.no'),
483 this.translate.instant('action.yes'), 483 this.translate.instant('action.yes'),
484 true 484 true
@@ -104,7 +104,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig @@ -104,7 +104,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
104 )); 104 ));
105 }; 105 };
106 this.config.onEntityAction = action => this.onEntityViewAction(action); 106 this.config.onEntityAction = action => this.onEntityViewAction(action);
107 - this.config.detailsReadonly = () => this.config.componentsData.entityViewScope === 'customer_user'; 107 + this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope === 'edge_customer_user');
108 108
109 this.config.headerComponent = EntityViewTableHeaderComponent; 109 this.config.headerComponent = EntityViewTableHeaderComponent;
110 110
@@ -152,7 +152,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig @@ -152,7 +152,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
152 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.entityViewScope); 152 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.entityViewScope);
153 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.entityViewScope); 153 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.entityViewScope);
154 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.entityViewScope); 154 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.entityViewScope);
155 - this.config.addEnabled = this.config.componentsData.entityViewScope !== 'customer_user'; 155 + this.config.addEnabled = !(this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope !== 'edge_customer_user');
156 this.config.entitiesDeleteEnabled = this.config.componentsData.entityViewScope === 'tenant'; 156 this.config.entitiesDeleteEnabled = this.config.componentsData.entityViewScope === 'tenant';
157 this.config.deleteEnabled = () => this.config.componentsData.entityViewScope === 'tenant'; 157 this.config.deleteEnabled = () => this.config.componentsData.entityViewScope === 'tenant';
158 return this.config; 158 return this.config;
@@ -57,13 +57,13 @@ @@ -57,13 +57,13 @@
57 </mat-form-field> 57 </mat-form-field>
58 <mat-form-field class="mat-block"> 58 <mat-form-field class="mat-block">
59 <mat-label translate>language.language</mat-label> 59 <mat-label translate>language.language</mat-label>
60 - <mat-select matInput formControlName="language"> 60 + <mat-select formControlName="language">
61 <mat-option *ngFor="let lang of languageList" [value]="lang"> 61 <mat-option *ngFor="let lang of languageList" [value]="lang">
62 {{ lang ? ('language.locales.' + lang | translate) : ''}} 62 {{ lang ? ('language.locales.' + lang | translate) : ''}}
63 </mat-option> 63 </mat-option>
64 </mat-select> 64 </mat-select>
65 </mat-form-field> 65 </mat-form-field>
66 - <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row"> 66 + <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row" *ngIf="!isSysAdmin()">
67 <tb-dashboard-autocomplete 67 <tb-dashboard-autocomplete
68 fxFlex 68 fxFlex
69 placeholder="{{ 'dashboard.home-dashboard' | translate }}" 69 placeholder="{{ 'dashboard.home-dashboard' | translate }}"
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 import { Component, OnInit } from '@angular/core'; 17 import { Component, OnInit } from '@angular/core';
18 import { UserService } from '@core/http/user.service'; 18 import { UserService } from '@core/http/user.service';
19 -import { User } from '@shared/models/user.model'; 19 +import { AuthUser, User } from '@shared/models/user.model';
20 import { Authority } from '@shared/models/authority.enum'; 20 import { Authority } from '@shared/models/authority.enum';
21 import { PageComponent } from '@shared/components/page.component'; 21 import { PageComponent } from '@shared/components/page.component';
22 import { Store } from '@ngrx/store'; 22 import { Store } from '@ngrx/store';
@@ -33,6 +33,7 @@ import { DialogService } from '@core/services/dialog.service'; @@ -33,6 +33,7 @@ import { DialogService } from '@core/services/dialog.service';
33 import { AuthService } from '@core/auth/auth.service'; 33 import { AuthService } from '@core/auth/auth.service';
34 import { ActivatedRoute } from '@angular/router'; 34 import { ActivatedRoute } from '@angular/router';
35 import { isDefinedAndNotNull } from '@core/utils'; 35 import { isDefinedAndNotNull } from '@core/utils';
  36 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
36 37
37 @Component({ 38 @Component({
38 selector: 'tb-profile', 39 selector: 'tb-profile',
@@ -45,6 +46,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -45,6 +46,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
45 profile: FormGroup; 46 profile: FormGroup;
46 user: User; 47 user: User;
47 languageList = env.supportedLangs; 48 languageList = env.supportedLangs;
  49 + private readonly authUser: AuthUser;
48 50
49 constructor(protected store: Store<AppState>, 51 constructor(protected store: Store<AppState>,
50 private route: ActivatedRoute, 52 private route: ActivatedRoute,
@@ -55,6 +57,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -55,6 +57,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
55 public dialogService: DialogService, 57 public dialogService: DialogService,
56 public fb: FormBuilder) { 58 public fb: FormBuilder) {
57 super(store); 59 super(store);
  60 + this.authUser = getCurrentAuthUser(this.store);
58 } 61 }
59 62
60 ngOnInit() { 63 ngOnInit() {
@@ -134,4 +137,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir @@ -134,4 +137,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
134 return this.profile; 137 return this.profile;
135 } 138 }
136 139
  140 + isSysAdmin(): boolean {
  141 + return this.authUser.authority === Authority.SYS_ADMIN;
  142 + }
  143 +
137 } 144 }
@@ -24,7 +24,6 @@ import { @@ -24,7 +24,6 @@ import {
24 import { Resolve } from '@angular/router'; 24 import { Resolve } from '@angular/router';
25 import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models'; 25 import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models';
26 import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; 26 import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
27 -import { Direction } from '@shared/models/page/sort-order';  
28 import { NULL_UUID } from '@shared/models/id/has-uuid'; 27 import { NULL_UUID } from '@shared/models/id/has-uuid';
29 import { DatePipe } from '@angular/common'; 28 import { DatePipe } from '@angular/common';
30 import { TranslateService } from '@ngx-translate/core'; 29 import { TranslateService } from '@ngx-translate/core';
@@ -34,13 +33,14 @@ import { Store } from '@ngrx/store'; @@ -34,13 +33,14 @@ import { Store } from '@ngrx/store';
34 import { AppState } from '@core/core.state'; 33 import { AppState } from '@core/core.state';
35 import { Authority } from '@shared/models/authority.enum'; 34 import { Authority } from '@shared/models/authority.enum';
36 import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component'; 35 import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component';
37 -import { Observable } from 'rxjs/internal/Observable';  
38 -import { PageData } from '@shared/models/page/page-data'; 36 +import { PageLink } from '@shared/models/page/page-link';
  37 +import { EntityAction } from '@home/models/entity/entity-component.models';
  38 +import { map } from 'rxjs/operators';
39 39
40 @Injectable() 40 @Injectable()
41 -export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource>> { 41 +export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource, PageLink, ResourceInfo>> {
42 42
43 - private readonly config: EntityTableConfig<Resource> = new EntityTableConfig<Resource>(); 43 + private readonly config: EntityTableConfig<Resource, PageLink, ResourceInfo> = new EntityTableConfig<Resource, PageLink, ResourceInfo>();
44 private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; 44 private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
45 45
46 constructor(private store: Store<AppState>, 46 constructor(private store: Store<AppState>,
@@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC @@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
52 this.config.entityComponent = ResourcesLibraryComponent; 52 this.config.entityComponent = ResourcesLibraryComponent;
53 this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE); 53 this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
54 this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); 54 this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
55 - this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};  
56 55
57 this.config.entityTitle = (resource) => resource ? 56 this.config.entityTitle = (resource) => resource ?
58 resource.title : ''; 57 resource.title : '';
59 58
60 this.config.columns.push( 59 this.config.columns.push(
61 new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'), 60 new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
62 - new EntityTableColumn<ResourceInfo>('title', 'widgets-bundle.title', '60%'), 61 + new EntityTableColumn<ResourceInfo>('title', 'resource.title', '60%'),
63 new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%', 62 new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
64 entity => this.resourceTypesTranslationMap.get(entity.resourceType)), 63 entity => this.resourceTypesTranslationMap.get(entity.resourceType)),
65 - new EntityTableColumn<ResourceInfo>('tenantId', 'widgets-bundle.system', '60px', 64 + new EntityTableColumn<ResourceInfo>('tenantId', 'resource.system', '60px',
66 entity => { 65 entity => {
67 return checkBoxCell(entity.tenantId.id === NULL_UUID); 66 return checkBoxCell(entity.tenantId.id === NULL_UUID);
68 }), 67 }),
@@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC @@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
83 this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count}); 82 this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
84 this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); 83 this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
85 84
86 - this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>; 85 + this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink);
87 this.config.loadEntity = id => this.resourceService.getResource(id.id); 86 this.config.loadEntity = id => this.resourceService.getResource(id.id);
88 - this.config.saveEntity = resource => this.resourceService.saveResource(resource); 87 + this.config.saveEntity = resource => this.saveResource(resource);
89 this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); 88 this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
  89 +
  90 + this.config.onEntityAction = action => this.onResourceAction(action);
  91 + }
  92 +
  93 + saveResource(resource) {
  94 + if (Array.isArray(resource.data)) {
  95 + const resources = [];
  96 + resource.data.forEach((data, index) => {
  97 + resources.push({
  98 + resourceType: resource.resourceType,
  99 + data,
  100 + fileName: resource.fileName[index],
  101 + title: resource.title
  102 + });
  103 + });
  104 + return this.resourceService.saveResources(resources, {resendRequest: true}).pipe(
  105 + map((response) => response[0])
  106 + );
  107 + } else {
  108 + return this.resourceService.saveResource(resource);
  109 + }
90 } 110 }
91 111
92 - resolve(): EntityTableConfig<Resource> { 112 + resolve(): EntityTableConfig<Resource, PageLink, ResourceInfo> {
93 this.config.tableTitle = this.translate.instant('resource.resources-library'); 113 this.config.tableTitle = this.translate.instant('resource.resources-library');
94 const authUser = getCurrentAuthUser(this.store); 114 const authUser = getCurrentAuthUser(this.store);
95 this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); 115 this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
@@ -105,7 +125,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC @@ -105,7 +125,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
105 this.resourceService.downloadResource(resource.id.id).subscribe(); 125 this.resourceService.downloadResource(resource.id.id).subscribe();
106 } 126 }
107 127
108 - private isResourceEditable(resource: Resource, authority: Authority): boolean { 128 + onResourceAction(action: EntityAction<ResourceInfo>): boolean {
  129 + switch (action.action) {
  130 + case 'uploadResource':
  131 + this.exportResource(action.event, action.entity);
  132 + return true;
  133 + }
  134 + return false;
  135 + }
  136 +
  137 + private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean {
109 if (authority === Authority.TENANT_ADMIN) { 138 if (authority === Authority.TENANT_ADMIN) {
110 return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID; 139 return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID;
111 } else { 140 } else {
@@ -18,6 +18,12 @@ @@ -18,6 +18,12 @@
18 <div class="tb-details-buttons" fxLayout.xs="column"> 18 <div class="tb-details-buttons" fxLayout.xs="column">
19 <button mat-raised-button color="primary" fxFlex.xs 19 <button mat-raised-button color="primary" fxFlex.xs
20 [disabled]="(isLoading$ | async)" 20 [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'uploadResource')"
  22 + [fxShow]="!isEdit">
  23 + {{'resource.export' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary" fxFlex.xs
  26 + [disabled]="(isLoading$ | async)"
21 (click)="onEntityAction($event, 'delete')" 27 (click)="onEntityAction($event, 'delete')"
22 [fxShow]="!hideDelete() && !isEdit"> 28 [fxShow]="!hideDelete() && !isEdit">
23 {{'resource.delete' | translate }} 29 {{'resource.delete' | translate }}
@@ -44,9 +50,11 @@ @@ -44,9 +50,11 @@
44 <tb-file-input 50 <tb-file-input
45 formControlName="data" 51 formControlName="data"
46 required 52 required
47 - [convertToBase64]="true" 53 + [readAsBinary]="true"
48 [allowedExtensions]="getAllowedExtensions()" 54 [allowedExtensions]="getAllowedExtensions()"
  55 + [contentConvertFunction]="convertToBase64File"
49 [accept]="getAcceptType()" 56 [accept]="getAcceptType()"
  57 + [multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"
50 dropLabel="{{'resource.drop-file' | translate}}" 58 dropLabel="{{'resource.drop-file' | translate}}"
51 [existingFileName]="entityForm.get('fileName')?.value" 59 [existingFileName]="entityForm.get('fileName')?.value"
52 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> 60 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
@@ -29,7 +29,7 @@ import { @@ -29,7 +29,7 @@ import {
29 ResourceTypeMIMETypes, 29 ResourceTypeMIMETypes,
30 ResourceTypeTranslationMap 30 ResourceTypeTranslationMap
31 } from '@shared/models/resource.models'; 31 } from '@shared/models/resource.models';
32 -import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; 32 +import { pairwise, startWith, takeUntil } from 'rxjs/operators';
33 33
34 @Component({ 34 @Component({
35 selector: 'tb-resources-library', 35 selector: 'tb-resources-library',
@@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
54 ngOnInit() { 54 ngOnInit() {
55 super.ngOnInit(); 55 super.ngOnInit();
56 this.entityForm.get('resourceType').valueChanges.pipe( 56 this.entityForm.get('resourceType').valueChanges.pipe(
57 - distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)), 57 + startWith(ResourceType.LWM2M_MODEL),
  58 + pairwise(),
58 takeUntil(this.destroy$) 59 takeUntil(this.destroy$)
59 - ).subscribe((type) => { 60 + ).subscribe(([previousType, type]) => {
  61 + if (previousType === this.resourceType.LWM2M_MODEL) {
  62 + this.entityForm.get('title').setValidators(Validators.required);
  63 + this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
  64 + }
60 if (type === this.resourceType.LWM2M_MODEL) { 65 if (type === this.resourceType.LWM2M_MODEL) {
61 this.entityForm.get('title').clearValidators(); 66 this.entityForm.get('title').clearValidators();
62 - } else {  
63 - this.entityForm.get('title').setValidators(Validators.required); 67 + this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
64 } 68 }
65 - this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); 69 + this.entityForm.patchValue({
  70 + data: null,
  71 + fileName: null
  72 + }, {emitEvent: false});
66 }); 73 });
67 } 74 }
68 75
@@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
119 return '*/*'; 126 return '*/*';
120 } 127 }
121 } 128 }
  129 +
  130 + convertToBase64File(data: string): string {
  131 + return window.btoa(data);
  132 + }
122 } 133 }
@@ -19,19 +19,19 @@ @@ -19,19 +19,19 @@
19 <button mat-raised-button color="primary" 19 <button mat-raised-button color="primary"
20 [disabled]="(isLoading$ | async)" 20 [disabled]="(isLoading$ | async)"
21 (click)="onEntityAction($event, 'open')" 21 (click)="onEntityAction($event, 'open')"
22 - [fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')"> 22 + [fxShow]="!isEdit">
23 {{'rulechain.open-rulechain' | translate }} 23 {{'rulechain.open-rulechain' | translate }}
24 </button> 24 </button>
25 <button mat-raised-button color="primary" 25 <button mat-raised-button color="primary"
26 [disabled]="(isLoading$ | async)" 26 [disabled]="(isLoading$ | async)"
27 (click)="onEntityAction($event, 'export')" 27 (click)="onEntityAction($event, 'export')"
28 - [fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')"> 28 + [fxShow]="!isEdit">
29 {{'rulechain.export' | translate }} 29 {{'rulechain.export' | translate }}
30 </button> 30 </button>
31 <button mat-raised-button color="primary" 31 <button mat-raised-button color="primary"
32 [disabled]="(isLoading$ | async)" 32 [disabled]="(isLoading$ | async)"
33 (click)="onEntityAction($event, 'setRoot')" 33 (click)="onEntityAction($event, 'setRoot')"
34 - [fxShow]="!isEdit && !entity?.root && ruleChainScope === 'tenant'"> 34 + [fxShow]="!isEdit && ((!entity?.root && ruleChainScope === 'tenant') || (!isEdgeRootRuleChain() && ruleChainScope === 'edge'))">
35 {{'rulechain.set-root' | translate }} 35 {{'rulechain.set-root' | translate }}
36 </button> 36 </button>
37 <button mat-raised-button color="primary" 37 <button mat-raised-button color="primary"
@@ -54,12 +54,6 @@ @@ -54,12 +54,6 @@
54 </button> 54 </button>
55 <button mat-raised-button color="primary" 55 <button mat-raised-button color="primary"
56 [disabled]="(isLoading$ | async)" 56 [disabled]="(isLoading$ | async)"
57 - (click)="onEntityAction($event, 'setRoot')"  
58 - [fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">  
59 - {{'rulechain.set-root' | translate }}  
60 - </button>  
61 - <button mat-raised-button color="primary"  
62 - [disabled]="(isLoading$ | async)"  
63 (click)="onEntityAction($event, 'unassignFromEdge')" 57 (click)="onEntityAction($event, 'unassignFromEdge')"
64 [fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'"> 58 [fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">
65 {{'edge.unassign-from-edge' | translate }} 59 {{'edge.unassign-from-edge' | translate }}
@@ -67,7 +61,7 @@ @@ -67,7 +61,7 @@
67 <button mat-raised-button color="primary" 61 <button mat-raised-button color="primary"
68 [disabled]="(isLoading$ | async)" 62 [disabled]="(isLoading$ | async)"
69 (click)="onEntityAction($event, 'delete')" 63 (click)="onEntityAction($event, 'delete')"
70 - [fxShow]="!hideDelete() && !isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')"> 64 + [fxShow]="!hideDelete() && !isEdit && ruleChainScope !== 'edge'">
71 {{'rulechain.delete' | translate }} 65 {{'rulechain.delete' | translate }}
72 </button> 66 </button>
73 <div fxLayout="row"> 67 <div fxLayout="row">
@@ -211,53 +211,51 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig< @@ -211,53 +211,51 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
211 211
212 configureCellActions(ruleChainScope: string): Array<CellActionDescriptor<RuleChain>> { 212 configureCellActions(ruleChainScope: string): Array<CellActionDescriptor<RuleChain>> {
213 const actions: Array<CellActionDescriptor<RuleChain>> = []; 213 const actions: Array<CellActionDescriptor<RuleChain>> = [];
214 - if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') { 214 + actions.push(
  215 + {
  216 + name: this.translate.instant('rulechain.open-rulechain'),
  217 + icon: 'settings_ethernet',
  218 + isEnabled: () => true,
  219 + onAction: ($event, entity) => this.openRuleChain($event, entity)
  220 + },
  221 + {
  222 + name: this.translate.instant('rulechain.export'),
  223 + icon: 'file_download',
  224 + isEnabled: () => true,
  225 + onAction: ($event, entity) => this.exportRuleChain($event, entity)
  226 + }
  227 + );
  228 + if (ruleChainScope === 'tenant') {
215 actions.push( 229 actions.push(
216 { 230 {
217 - name: this.translate.instant('rulechain.open-rulechain'),  
218 - icon: 'settings_ethernet',  
219 - isEnabled: () => true,  
220 - onAction: ($event, entity) => this.openRuleChain($event, entity) 231 + name: this.translate.instant('rulechain.set-root'),
  232 + icon: 'flag',
  233 + isEnabled: (entity) => this.isNonRootRuleChain(entity),
  234 + onAction: ($event, entity) => this.setRootRuleChain($event, entity)
  235 + }
  236 + );
  237 + }
  238 + if (ruleChainScope === 'edges') {
  239 + actions.push(
  240 + {
  241 + name: this.translate.instant('rulechain.set-edge-template-root-rulechain'),
  242 + icon: 'flag',
  243 + isEnabled: (entity) => this.isNonRootRuleChain(entity),
  244 + onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity)
221 }, 245 },
222 { 246 {
223 - name: this.translate.instant('rulechain.export'),  
224 - icon: 'file_download',  
225 - isEnabled: () => true,  
226 - onAction: ($event, entity) => this.exportRuleChain($event, entity) 247 + name: this.translate.instant('rulechain.set-auto-assign-to-edge'),
  248 + icon: 'bookmark_outline',
  249 + isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity),
  250 + onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity)
  251 + },
  252 + {
  253 + name: this.translate.instant('rulechain.unset-auto-assign-to-edge'),
  254 + icon: 'bookmark',
  255 + isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity),
  256 + onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity)
227 } 257 }
228 ); 258 );
229 - if (ruleChainScope === 'tenant') {  
230 - actions.push(  
231 - {  
232 - name: this.translate.instant('rulechain.set-root'),  
233 - icon: 'flag',  
234 - isEnabled: (entity) => this.isNonRootRuleChain(entity),  
235 - onAction: ($event, entity) => this.setRootRuleChain($event, entity)  
236 - }  
237 - );  
238 - }  
239 - if (ruleChainScope === 'edges') {  
240 - actions.push(  
241 - {  
242 - name: this.translate.instant('rulechain.set-edge-template-root-rulechain'),  
243 - icon: 'flag',  
244 - isEnabled: (entity) => this.isNonRootRuleChain(entity),  
245 - onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity)  
246 - },  
247 - {  
248 - name: this.translate.instant('rulechain.set-auto-assign-to-edge'),  
249 - icon: 'bookmark_outline',  
250 - isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity),  
251 - onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity)  
252 - },  
253 - {  
254 - name: this.translate.instant('rulechain.unset-auto-assign-to-edge'),  
255 - icon: 'bookmark',  
256 - isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity),  
257 - onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity)  
258 - }  
259 - );  
260 - }  
261 } 259 }
262 if (ruleChainScope === 'edge') { 260 if (ruleChainScope === 'edge') {
263 actions.push( 261 actions.push(
@@ -301,6 +299,8 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig< @@ -301,6 +299,8 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
301 } 299 }
302 if (this.config.componentsData.ruleChainScope === 'edges') { 300 if (this.config.componentsData.ruleChainScope === 'edges') {
303 this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`); 301 this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`);
  302 + } else if (this.config.componentsData.ruleChainScope === 'edge') {
  303 + this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`);
304 } else { 304 } else {
305 this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`); 305 this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`);
306 } 306 }
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
18 <div class="tb-container"> 18 <div class="tb-container">
19 <label class="tb-title">{{ label }}</label> 19 <label class="tb-title">{{ label }}</label>
20 <ng-container #flow="flow" 20 <ng-container #flow="flow"
21 - [flowConfig]="{singleFile: true, allowDuplicateUploads: true}"> 21 + [flowConfig]="{allowDuplicateUploads: true}">
22 <div class="tb-file-select-container"> 22 <div class="tb-file-select-container">
23 <div class="tb-file-clear-container"> 23 <div class="tb-file-clear-container">
24 <button mat-button mat-icon-button color="primary" 24 <button mat-button mat-icon-button color="primary"
@@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
34 flowDrop 34 flowDrop
35 [flow]="flow.flowJs"> 35 [flow]="flow.flowJs">
36 <label for="{{inputId}}">{{ dropLabel }}</label> 36 <label for="{{inputId}}">{{ dropLabel }}</label>
37 - <input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}"> 37 + <input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
38 </div> 38 </div>
39 </div> 39 </div>
40 </ng-container> 40 </ng-container>
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 import { 17 import {
18 AfterViewInit, 18 AfterViewInit,
19 Component, 19 Component,
  20 + ElementRef,
20 EventEmitter, 21 EventEmitter,
21 forwardRef, 22 forwardRef,
22 Input, 23 Input,
@@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
102 existingFileName: string; 103 existingFileName: string;
103 104
104 @Input() 105 @Input()
105 - convertToBase64 = false; 106 + readAsBinary = false;
  107 +
  108 + private multipleFileValue = false;
  109 +
  110 + @Input()
  111 + set multipleFile(value: boolean) {
  112 + this.multipleFileValue = value;
  113 + if (this.flow?.flowJs) {
  114 + this.updateMultipleFileMode(this.multipleFile);
  115 + }
  116 + }
  117 +
  118 + get multipleFile(): boolean {
  119 + return this.multipleFileValue;
  120 + }
106 121
107 @Output() 122 @Output()
108 - fileNameChanged = new EventEmitter<string>(); 123 + fileNameChanged = new EventEmitter<string|string[]>();
109 124
110 - fileName: string; 125 + fileName: string | string[];
111 fileContent: any; 126 fileContent: any;
112 127
113 @ViewChild('flow', {static: true}) 128 @ViewChild('flow', {static: true})
114 flow: FlowDirective; 129 flow: FlowDirective;
115 130
  131 + @ViewChild('flowInput', {static: true})
  132 + flowInput: ElementRef;
  133 +
116 autoUploadSubscription: Subscription; 134 autoUploadSubscription: Subscription;
117 135
118 private propagateChange = null; 136 private propagateChange = null;
@@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
125 143
126 ngAfterViewInit() { 144 ngAfterViewInit() {
127 this.autoUploadSubscription = this.flow.events$.subscribe(event => { 145 this.autoUploadSubscription = this.flow.events$.subscribe(event => {
128 - if (event.type === 'fileAdded') {  
129 - const file = event.event[0] as flowjs.FlowFile;  
130 - if (this.filterFile(file)) {  
131 - const reader = new FileReader();  
132 - reader.onload = (loadEvent) => {  
133 - if (typeof reader.result === 'string') {  
134 - const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result;  
135 - if (fileContent && fileContent.length > 0) {  
136 - if (this.contentConvertFunction) {  
137 - this.fileContent = this.contentConvertFunction(fileContent);  
138 - } else {  
139 - this.fileContent = fileContent;  
140 - }  
141 - if (this.fileContent) {  
142 - this.fileName = file.name;  
143 - } else {  
144 - this.fileName = null;  
145 - }  
146 - this.updateModel();  
147 - } 146 + if (event.type === 'filesAdded') {
  147 + const readers = [];
  148 + (event.event[0] as flowjs.FlowFile[]).forEach(file => {
  149 + if (this.filterFile(file)) {
  150 + readers.push(this.readerAsFile(file));
  151 + }
  152 + });
  153 + if (readers.length) {
  154 + Promise.all(readers).then((filesContent) => {
  155 + filesContent = filesContent.filter(content => content.fileContent != null);
  156 + if (filesContent.length === 1) {
  157 + this.fileContent = filesContent[0].fileContent;
  158 + this.fileName = filesContent[0].fileName;
  159 + this.updateModel();
  160 + } else if (filesContent.length > 1) {
  161 + this.fileContent = filesContent.map(content => content.fileContent);
  162 + this.fileName = filesContent.map(content => content.fileName);
  163 + this.updateModel();
  164 + }
  165 + });
  166 + }
  167 + }
  168 + });
  169 + if (!this.multipleFile) {
  170 + this.updateMultipleFileMode(this.multipleFile);
  171 + }
  172 + }
  173 +
  174 + private readerAsFile(file: flowjs.FlowFile): Promise<any> {
  175 + return new Promise((resolve) => {
  176 + const reader = new FileReader();
  177 + reader.onload = () => {
  178 + let fileName = null;
  179 + let fileContent = null;
  180 + if (typeof reader.result === 'string') {
  181 + fileContent = reader.result;
  182 + if (fileContent && fileContent.length > 0) {
  183 + if (this.contentConvertFunction) {
  184 + fileContent = this.contentConvertFunction(fileContent);
  185 + }
  186 + if (fileContent) {
  187 + fileName = file.name;
148 } 188 }
149 - };  
150 - if (this.convertToBase64) {  
151 - reader.readAsBinaryString(file.file);  
152 - } else {  
153 - reader.readAsText(file.file);  
154 } 189 }
155 } 190 }
  191 + resolve({fileContent, fileName});
  192 + };
  193 + reader.onerror = () => {
  194 + resolve({fileContent: null, fileName: null});
  195 + };
  196 + if (this.readAsBinary) {
  197 + reader.readAsBinaryString(file.file);
  198 + } else {
  199 + reader.readAsText(file.file);
156 } 200 }
157 }); 201 });
158 } 202 }
@@ -207,4 +251,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -207,4 +251,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
207 this.fileContent = null; 251 this.fileContent = null;
208 this.updateModel(); 252 this.updateModel();
209 } 253 }
  254 +
  255 + private updateMultipleFileMode(multiple: boolean) {
  256 + this.flow.flowJs.opts.singleFile = !multiple;
  257 + if (!multiple) {
  258 + this.flowInput.nativeElement.removeAttribute('multiple');
  259 + }
  260 + }
210 } 261 }
@@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati @@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati
22 import { Store } from '@ngrx/store'; 22 import { Store } from '@ngrx/store';
23 import { AppState } from '@core/core.state'; 23 import { AppState } from '@core/core.state';
24 import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; 24 import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
25 -import { guid, isUndefined } from '@core/utils'; 25 +import { guid, isUndefined, isDefinedAndNotNull, isLiteralObject } from '@core/utils';
26 import { ResizeObserver } from '@juggle/resize-observer'; 26 import { ResizeObserver } from '@juggle/resize-observer';
27 import { getAce } from '@shared/models/ace/ace.models'; 27 import { getAce } from '@shared/models/ace/ace.models';
28 28
@@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
230 this.contentValue = ''; 230 this.contentValue = '';
231 this.objectValid = false; 231 this.objectValid = false;
232 try { 232 try {
233 -  
234 - if (this.modelValue) { 233 + if (isDefinedAndNotNull(this.modelValue)) {
235 this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : 234 this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined :
236 (key, objectValue) => { 235 (key, objectValue) => {
237 return this.sort(key, objectValue); 236 return this.sort(key, objectValue);
@@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
260 if (this.contentValue && this.contentValue.length > 0) { 259 if (this.contentValue && this.contentValue.length > 0) {
261 try { 260 try {
262 data = JSON.parse(this.contentValue); 261 data = JSON.parse(this.contentValue);
  262 + if (!isLiteralObject(data)) {
  263 + throw new TypeError(`Value is not a valid JSON`);
  264 + }
263 this.objectValid = true; 265 this.objectValid = true;
264 this.validationError = ''; 266 this.validationError = '';
265 } catch (ex) { 267 } catch (ex) {
@@ -15,8 +15,9 @@ @@ -15,8 +15,9 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<section fxLayout="column" fxLayoutAlign="start start">  
19 - <section fxLayout="row" fxLayout.xs="row wrap" fxLayoutAlign="start start" fxLayoutGap="16px"> 18 +<section fxLayout="column" fxLayoutAlign="start stretch">
  19 + <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
  20 + fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
20 <mat-form-field> 21 <mat-form-field>
21 <mat-placeholder translate>datetime.date-from</mat-placeholder> 22 <mat-placeholder translate>datetime.date-from</mat-placeholder>
22 <mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle> 23 <mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
@@ -30,7 +31,8 @@ @@ -30,7 +31,8 @@
30 <input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()"> 31 <input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()">
31 </mat-form-field> 32 </mat-form-field>
32 </section> 33 </section>
33 - <section fxLayout="row" fxLayout.xs="row wrap" fxLayoutAlign="start start" fxLayoutGap="16px"> 34 + <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
  35 + fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
34 <mat-form-field> 36 <mat-form-field>
35 <mat-placeholder translate>datetime.date-to</mat-placeholder> 37 <mat-placeholder translate>datetime.date-to</mat-placeholder>
36 <mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle> 38 <mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
@@ -21,7 +21,6 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode @@ -21,7 +21,6 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode
21 @Component({ 21 @Component({
22 selector: 'tb-quick-time-interval', 22 selector: 'tb-quick-time-interval',
23 templateUrl: './quick-time-interval.component.html', 23 templateUrl: './quick-time-interval.component.html',
24 - styleUrls: ['./quick-time-interval.component.scss'],  
25 providers: [ 24 providers: [
26 { 25 {
27 provide: NG_VALUE_ACCESSOR, 26 provide: NG_VALUE_ACCESSOR,
@@ -51,7 +51,7 @@ @@ -51,7 +51,7 @@
51 [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" 51 [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
52 [required]="timewindow.selectedTab === timewindowTypes.REALTIME && 52 [required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
53 timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" 53 timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
54 - style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval> 54 + style="padding-top: 8px"></tb-quick-time-interval>
55 </section> 55 </section>
56 </mat-radio-button> 56 </mat-radio-button>
57 </mat-radio-group> 57 </mat-radio-group>
@@ -75,6 +75,7 @@ @@ -75,6 +75,7 @@
75 <tb-timeinterval 75 <tb-timeinterval
76 formControlName="timewindowMs" 76 formControlName="timewindowMs"
77 predefinedName="timewindow.last" 77 predefinedName="timewindow.last"
  78 + class="history-time-input"
78 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL" 79 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
79 [required]="timewindow.selectedTab === timewindowTypes.HISTORY && 80 [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
80 timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL" 81 timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
@@ -86,6 +87,7 @@ @@ -86,6 +87,7 @@
86 <span translate>timewindow.time-period</span> 87 <span translate>timewindow.time-period</span>
87 <tb-datetime-period 88 <tb-datetime-period
88 formControlName="fixedTimewindow" 89 formControlName="fixedTimewindow"
  90 + class="history-time-input"
89 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED" 91 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
90 [required]="timewindow.selectedTab === timewindowTypes.HISTORY && 92 [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
91 timewindowForm.get('history.historyType').value === historyTypes.FIXED" 93 timewindowForm.get('history.historyType').value === historyTypes.FIXED"
@@ -97,10 +99,11 @@ @@ -97,10 +99,11 @@
97 <span translate>timewindow.interval</span> 99 <span translate>timewindow.interval</span>
98 <tb-quick-time-interval 100 <tb-quick-time-interval
99 formControlName="quickInterval" 101 formControlName="quickInterval"
  102 + class="history-time-input"
100 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" 103 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
101 [required]="timewindow.selectedTab === timewindowTypes.HISTORY && 104 [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
102 timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" 105 timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
103 - style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval> 106 + style="padding-top: 8px"></tb-quick-time-interval>
104 </section> 107 </section>
105 </mat-radio-button> 108 </mat-radio-button>
106 </mat-radio-group> 109 </mat-radio-group>
@@ -134,21 +137,23 @@ @@ -134,21 +137,23 @@
134 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox> 137 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
135 </section> 138 </section>
136 <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval"> 139 <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
137 - <div class="limit-slider-container"  
138 - fxLayout="row" fxLayoutAlign="start center">  
139 - <span translate>aggregation.limit</span>  
140 - <mat-slider fxFlex formControlName="limit"  
141 - thumbLabel  
142 - [value]="timewindowForm.get('aggregation.limit').value"  
143 - min="{{minDatapointsLimit()}}"  
144 - max="{{maxDatapointsLimit()}}">  
145 - </mat-slider>  
146 - <mat-form-field style="max-width: 80px;">  
147 - <input matInput formControlName="limit" type="number" step="1"  
148 - [value]="timewindowForm.get('aggregation.limit').value"  
149 - min="{{minDatapointsLimit()}}"  
150 - max="{{maxDatapointsLimit()}}"/>  
151 - </mat-form-field> 140 + <div class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center"
  141 + fxLayout.xs="column" fxLayoutAlign.xs="stretch">
  142 + <label translate>aggregation.limit</label>
  143 + <div fxLayout="row" fxLayoutAlign="start center" fxFlex>
  144 + <mat-slider fxFlex formControlName="limit"
  145 + thumbLabel
  146 + [value]="timewindowForm.get('aggregation.limit').value"
  147 + min="{{minDatapointsLimit()}}"
  148 + max="{{maxDatapointsLimit()}}">
  149 + </mat-slider>
  150 + <mat-form-field class="limit-slider-value">
  151 + <input matInput formControlName="limit" type="number" step="1"
  152 + [value]="timewindowForm.get('aggregation.limit').value"
  153 + min="{{minDatapointsLimit()}}"
  154 + max="{{maxDatapointsLimit()}}"/>
  155 + </mat-form-field>
  156 + </div>
152 </div> 157 </div>
153 </section> 158 </section>
154 </section> 159 </section>
@@ -13,6 +13,8 @@ @@ -13,6 +13,8 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
  16 +@import "../../../../scss/constants";
  17 +
16 :host { 18 :host {
17 width: 100%; 19 width: 100%;
18 height: 100%; 20 height: 100%;
@@ -40,21 +42,29 @@ @@ -40,21 +42,29 @@
40 } 42 }
41 43
42 .limit-slider-container { 44 .limit-slider-container {
43 - >:first-child {  
44 - margin-right: 16px;  
45 - }  
46 - >:last-child { 45 + .limit-slider-value {
47 margin-left: 16px; 46 margin-left: 16px;
48 - }  
49 - >:first-child, >:last-child {  
50 min-width: 25px; 47 min-width: 25px;
51 - max-width: 42px; 48 + max-width: 80px;
52 } 49 }
53 mat-form-field input[type=number] { 50 mat-form-field input[type=number] {
54 text-align: center; 51 text-align: center;
55 } 52 }
56 } 53 }
57 54
  55 + @media #{$mat-gt-sm} {
  56 + .history-time-input {
  57 + min-width: 364px;
  58 + }
  59 + .limit-slider-container {
  60 + > label {
  61 + margin-right: 16px;
  62 + width: min-content;
  63 + max-width: 40%;
  64 + }
  65 + }
  66 + }
  67 +
58 } 68 }
59 69
60 :host ::ng-deep { 70 :host ::ng-deep {
@@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo { @@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo {
59 data: string; 59 data: string;
60 fileName: string; 60 fileName: string;
61 } 61 }
  62 +
  63 +export interface Resources extends ResourceInfo {
  64 + data: string|string[];
  65 + fileName: string|string[];
  66 +}
@@ -141,6 +141,7 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; @@ -141,6 +141,7 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe';
141 import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; 141 import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
142 import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; 142 import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
143 import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component'; 143 import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
  144 +import { MAT_DATE_LOCALE } from '@angular/material/core';
144 145
145 @NgModule({ 146 @NgModule({
146 providers: [ 147 providers: [
@@ -154,6 +155,10 @@ import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-i @@ -154,6 +155,10 @@ import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-i
154 { 155 {
155 provide: FlowInjectionToken, 156 provide: FlowInjectionToken,
156 useValue: Flow 157 useValue: Flow
  158 + },
  159 + {
  160 + provide: MAT_DATE_LOCALE,
  161 + useValue: 'en-GB'
157 } 162 }
158 ], 163 ],
159 declarations: [ 164 declarations: [
@@ -1943,7 +1943,7 @@ @@ -1943,7 +1943,7 @@
1943 "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }", 1943 "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }",
1944 "set-root": "设置为根规则链", 1944 "set-root": "设置为根规则链",
1945 "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", 1945 "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。",
1946 - "set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?", 1946 + "set-root-rulechain-title": "您确定要生成规则链'{{ruleChainName}}'根吗?",
1947 "system": "系统" 1947 "system": "系统"
1948 }, 1948 },
1949 "rulenode": { 1949 "rulenode": {