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 455 "dataKeySettingsSchema": "{}\n",
456 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 21 @Controller
22 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 25 public String redirect() {
26 26 return "forward:/index.html";
27 27 }
... ...
... ... @@ -17,8 +17,10 @@ package org.thingsboard.server.service.install;
17 17
18 18 import com.fasterxml.jackson.databind.ObjectMapper;
19 19 import com.fasterxml.jackson.databind.node.ObjectNode;
  20 +import lombok.Getter;
20 21 import lombok.extern.slf4j.Slf4j;
21 22 import org.springframework.beans.factory.annotation.Autowired;
  23 +import org.springframework.beans.factory.annotation.Value;
22 24 import org.springframework.context.annotation.Bean;
23 25 import org.springframework.context.annotation.Profile;
24 26 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
... ...
... ... @@ -40,7 +40,7 @@ import org.thingsboard.server.transport.lwm2m.utils.TypeServer;
40 40
41 41 import java.io.IOException;
42 42 import java.security.GeneralSecurityException;
43   -import java.util.Arrays;
  43 +import java.util.Collections;
44 44 import java.util.List;
45 45 import java.util.UUID;
46 46
... ... @@ -70,8 +70,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
70 70
71 71 @Override
72 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 74 if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) {
76 75 /** add value to store from BootstrapJson */
77 76 this.setBootstrapConfigScurityInfo(store);
... ... @@ -87,7 +86,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
87 86 } catch (InvalidConfigurationException e) {
88 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 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 13 * See the License for the specific language governing permissions and
14 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 16 package org.thingsboard.server.transport.lwm2m.server;
17 17
18 18 import lombok.extern.slf4j.Slf4j;
  19 +import org.eclipse.californium.core.coap.CoAP;
19 20 import org.eclipse.californium.core.coap.Response;
20 21 import org.eclipse.leshan.core.attributes.Attribute;
21 22 import org.eclipse.leshan.core.attributes.AttributeSet;
... ... @@ -34,6 +35,7 @@ import org.eclipse.leshan.core.request.ObserveRequest;
34 35 import org.eclipse.leshan.core.request.ReadRequest;
35 36 import org.eclipse.leshan.core.request.WriteAttributesRequest;
36 37 import org.eclipse.leshan.core.request.WriteRequest;
  38 +import org.eclipse.leshan.core.request.exception.ClientSleepingException;
37 39 import org.eclipse.leshan.core.response.CancelObservationResponse;
38 40 import org.eclipse.leshan.core.response.DeleteResponse;
39 41 import org.eclipse.leshan.core.response.DiscoverResponse;
... ... @@ -58,7 +60,6 @@ import java.util.Date;
58 60 import java.util.concurrent.ExecutorService;
59 61 import java.util.concurrent.Executors;
60 62
61   -import static org.eclipse.californium.core.coap.CoAP.ResponseCode.isSuccess;
62 63 import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD;
63 64 import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT;
64 65 import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER;
... ... @@ -215,13 +216,19 @@ public class LwM2mTransportRequest {
215 216 request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet);
216 217 }
217 218 break;
218   - default:
219 219 }
220 220
221 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 232 log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer);
226 233 }
227 234 }
... ... @@ -240,7 +247,7 @@ public class LwM2mTransportRequest {
240 247 if (!lwM2MClient.isInit()) {
241 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 251 this.handleResponse(registration, request.getPath().toString(), response, request);
245 252 if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) {
246 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 290 case FLOAT: // Double
284 291 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString()));
285 292 case TIME: // Date
286   - Date date = new Date(Long.decode(value.toString()));
  293 + Date date = new Date(Long.decode(value.toString()));
287 294 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date);
288 295 case OPAQUE: // byte[] value, base64
289 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 146 * Next -> Create new LwM2MClient for current session -> setModelClient...
147 147 *
148 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 152 executorRegistered.submit(() -> {
153 153 try {
154 154 log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId());
... ... @@ -188,6 +188,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
188 188 LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo);
189 189 if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) {
190 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 200 log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType());
... ... @@ -206,7 +213,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
206 213 * !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect
207 214 */
208 215 public void unReg(Registration registration, Collection<Observation> observations) {
209   - executorUnRegistered.submit(() -> {
  216 + executorUnRegistered.submit(() -> {
210 217 try {
211 218 this.setCancelObservations(registration);
212 219 this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration);
... ... @@ -239,8 +246,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
239 246 }
240 247 }
241 248
  249 + @Override
242 250 public void onSleepingDev(Registration registration) {
243 251 log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint());
  252 + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is sleeping!", registration);
  253 +
244 254 //TODO: associate endpointId with device information.
245 255 }
246 256
... ... @@ -417,6 +427,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
417 427 */
418 428 protected void onAwakeDev(Registration registration) {
419 429 log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint());
  430 + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is awake!", registration);
420 431 //TODO: associate endpointId with device information.
421 432 }
422 433
... ... @@ -612,7 +623,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
612 623 if (GET_TYPE_OPER_READ.equals(typeOper)) {
613 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 628 } else {
618 629 result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() {
... ...
... ... @@ -25,13 +25,16 @@ import org.eclipse.leshan.server.registration.Registration;
25 25 import org.eclipse.leshan.server.security.SecurityInfo;
26 26 import org.thingsboard.server.gen.transport.TransportProtos;
27 27 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
  28 +import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest;
28 29 import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl;
29 30
30 31 import java.util.List;
31 32 import java.util.Map;
  33 +import java.util.Queue;
32 34 import java.util.Set;
33 35 import java.util.UUID;
34 36 import java.util.concurrent.ConcurrentHashMap;
  37 +import java.util.concurrent.ConcurrentLinkedQueue;
35 38 import java.util.concurrent.CopyOnWriteArrayList;
36 39 import java.util.stream.Collectors;
37 40
... ... @@ -54,6 +57,7 @@ public class LwM2mClient implements Cloneable {
54 57 private final Map<String, ResourceValue> resources;
55 58 private final Map<String, TransportProtos.TsKvProto> delayedRequests;
56 59 private final List<String> pendingRequests;
  60 + private final Queue<LwM2mQueuedRequest> queuedRequests;
57 61 private boolean init;
58 62
59 63 public Object clone() throws CloneNotSupportedException {
... ... @@ -71,6 +75,7 @@ public class LwM2mClient implements Cloneable {
71 75 this.profileId = profileId;
72 76 this.sessionId = sessionId;
73 77 this.init = false;
  78 + this.queuedRequests = new ConcurrentLinkedQueue<>();
74 79 }
75 80
76 81 public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) {
... ...
... ... @@ -51,11 +51,11 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
51 51 TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
52 52 try {
53 53 componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
  54 + transactionManager.commit(transaction);
54 55 } catch (Throwable th) {
55 56 log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType());
56 57 transactionManager.rollback(transaction);
57 58 }
58   - transactionManager.commit(transaction);
59 59 } else {
60 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 51 TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
52 52 try {
53 53 eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
  54 + transactionManager.commit(transaction);
54 55 } catch (Throwable th) {
55 56 log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
56 57 transactionManager.rollback(transaction);
57 58 }
58   - transactionManager.commit(transaction);
59 59 } else {
60 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 62 import java.util.List;
63 63 import java.util.Optional;
64 64 import java.util.concurrent.TimeUnit;
  65 +import java.util.concurrent.locks.Lock;
  66 +import java.util.concurrent.locks.ReentrantLock;
65 67 import java.util.stream.Collectors;
66 68
67 69 import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
... ... @@ -107,6 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
107 109 private PreparedStatement[] fetchStmtsDesc;
108 110 private PreparedStatement deleteStmt;
109 111 private PreparedStatement deletePartitionStmt;
  112 + private final Lock stmtCreationLock = new ReentrantLock();
110 113
111 114 private boolean isInstall() {
112 115 return environment.acceptsProfiles(Profiles.of("install"));
... ... @@ -545,13 +548,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
545 548
546 549 private PreparedStatement getDeleteStmt() {
547 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 566 return deleteStmt;
557 567 }
... ... @@ -585,27 +595,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
585 595
586 596 private PreparedStatement getDeletePartitionStmt() {
587 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 611 return deletePartitionStmt;
595 612 }
596 613
597 614 private PreparedStatement getSaveStmt(DataType dataType) {
598 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 635 return saveStmts[dataType.ordinal()];
... ... @@ -613,16 +637,23 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
613 637
614 638 private PreparedStatement getSaveTtlStmt(DataType dataType) {
615 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 659 return saveTtlStmts[dataType.ordinal()];
... ... @@ -630,24 +661,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
630 661
631 662 private PreparedStatement getPartitionInsertStmt() {
632 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 678 return partitionInsertStmt;
641 679 }
642 680
643 681 private PreparedStatement getPartitionInsertTtlStmt() {
644 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 697 return partitionInsertTtlStmt;
653 698 }
... ... @@ -713,12 +758,26 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
713 758 switch (orderBy) {
714 759 case ASC_ORDER:
715 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 770 return fetchStmtsAsc[aggType.ordinal()];
719 771 case DESC_ORDER:
720 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 782 return fetchStmtsDesc[aggType.ordinal()];
724 783 default:
... ...
... ... @@ -10,6 +10,7 @@ To run the black box tests with using Docker, the local Docker images of Thingsb
10 10 As result, in REPOSITORY column, next images should be present:
11 11
12 12 thingsboard/tb-coap-transport
  13 + thingsboard/tb-lwm2m-transport
13 14 thingsboard/tb-http-transport
14 15 thingsboard/tb-mqtt-transport
15 16 thingsboard/tb-node
... ...
... ... @@ -17,15 +17,14 @@
17 17 FROM thingsboard/openjdk11
18 18
19 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 22 RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
24 23 RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
25 24 RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add -
26   -ENV PG_MAJOR=11
  25 +ENV PG_MAJOR=12
27 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 28 RUN update-rc.d cassandra disable
30 29 RUN update-rc.d postgresql disable
31 30 RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra
... ...
... ... @@ -17,13 +17,12 @@
17 17 FROM thingsboard/openjdk11
18 18
19 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 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 24 RUN apt-get update
26   -RUN apt-get install -y postgresql-11
  25 +RUN apt-get install -y postgresql-12
27 26 RUN update-rc.d postgresql disable
28 27
29 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 39 <tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
40 40 <tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
41 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 43 </properties>
44 44
45 45 <dependencies>
... ...
... ... @@ -99,7 +99,7 @@
99 99 org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
100 100 </sonar.exclusions>
101 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 103 <kafka.version>2.6.0</kafka.version>
104 104 <bucket4j.version>4.1.1</bucket4j.version>
105 105 <fst.version>2.57</fst.version>
... ...
... ... @@ -1338,7 +1338,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1338 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 1342 Map<String, String> params = new HashMap<>();
1343 1343 addPageLinkToParam(params, pageLink);
1344 1344 return restTemplate.exchange(
... ...
... ... @@ -18,10 +18,10 @@ import { Injectable } from '@angular/core';
18 18 import { HttpClient } from '@angular/common/http';
19 19 import { PageLink } from '@shared/models/page/page-link';
20 20 import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
21   -import { Observable } from 'rxjs';
  21 +import { forkJoin, Observable, of } from 'rxjs';
22 22 import { PageData } from '@shared/models/page/page-data';
23 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 26 @Injectable({
27 27 providedIn: 'root'
... ... @@ -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 92 public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
74 93 return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
75 94 }
... ...
... ... @@ -79,7 +79,7 @@ export class TimeService {
79 79
80 80 public boundMinInterval(min: number): number {
81 81 if (isDefined(min)) {
82   - min = Math.floor(min / 1000) * 1000;
  82 + min = Math.ceil(min / 1000) * 1000;
83 83 }
84 84 return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL);
85 85 }
... ...
... ... @@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean {
127 127 return true;
128 128 }
129 129
  130 +export function isLiteralObject(value: any) {
  131 + return (!!value) && (value.constructor === Object);
  132 +}
  133 +
130 134 export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
131 135 if (isDefinedAndNotNull(value) && isNumeric(value) &&
132 136 (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) {
... ...
... ... @@ -90,7 +90,6 @@ import {
90 90 } from '@home/components/alias/entity-aliases-dialog.component';
91 91 import { EntityAliases } from '@app/shared/models/alias.models';
92 92 import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component';
93   -import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
94 93 import {
95 94 AddWidgetDialogComponent,
96 95 AddWidgetDialogData
... ... @@ -118,8 +117,7 @@ import { ComponentPortal } from '@angular/cdk/portal';
118 117 import {
119 118 DISPLAY_WIDGET_TYPES_PANEL_DATA,
120 119 DisplayWidgetTypesPanelComponent,
121   - DisplayWidgetTypesPanelData,
122   - WidgetTypes
  120 + DisplayWidgetTypesPanelData
123 121 } from '@home/components/dashboard-page/widget-types-panel.component';
124 122 import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
125 123 import {AliasEntityType, EntityType} from "@shared/models/entity-type.models";
... ... @@ -1189,13 +1187,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
1189 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 1195 const providers: StaticProvider[] = [
1193 1196 {
1194 1197 provide: DISPLAY_WIDGET_TYPES_PANEL_DATA,
1195 1198 useValue: {
1196   - types: Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => {
1197   - return {type, display: true};
1198   - }),
  1199 + types: widgetTypesList,
1199 1200 typesUpdated: (newTypes) => {
1200 1201 this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type);
1201 1202 }
... ...
... ... @@ -77,6 +77,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
77 77 this.filterWidgetTypes$.next(widgetTypes);
78 78 }
79 79
  80 + get filterWidgetTypes(): Array<widgetType> {
  81 + return this.filterWidgetTypes$.value;
  82 + }
  83 +
80 84 @Output()
81 85 widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();
82 86
... ...
... ... @@ -80,7 +80,7 @@
80 80 (mousedown)="widgetMouseDown($event, widget)"
81 81 (click)="widgetClicked($event, widget)"
82 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 84 <div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel">
85 85 <span *ngIf="widget.showTitle"
86 86 [ngClass]="{'single-row': widget.hasTimewindow}"
... ...
... ... @@ -15,11 +15,21 @@
15 15 ///
16 16
17 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 28 import {
20 29 BooleanFilterPredicate,
21 30 BooleanOperation,
22   - booleanOperationTranslationMap, EntityKeyValueType,
  31 + booleanOperationTranslationMap,
  32 + EntityKeyValueType,
23 33 FilterPredicateType
24 34 } from '@shared/models/query/query.models';
25 35
... ... @@ -32,10 +42,15 @@ import {
32 42 provide: NG_VALUE_ACCESSOR,
33 43 useExisting: forwardRef(() => BooleanFilterPredicateComponent),
34 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 55 @Input() disabled: boolean;
41 56
... ... @@ -73,7 +88,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
73 88 registerOnTouched(fn: any): void {
74 89 }
75 90
76   - setDisabledState?(isDisabled: boolean): void {
  91 + setDisabledState(isDisabled: boolean): void {
77 92 this.disabled = isDisabled;
78 93 if (this.disabled) {
79 94 this.booleanFilterPredicateFormGroup.disable({emitEvent: false});
... ... @@ -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 106 writeValue(predicate: BooleanFilterPredicate): void {
86 107 this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
87 108 this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
88 109 }
89 110
90 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 114 this.propagateChange(predicate);
97 115 }
98 116
... ...
... ... @@ -16,11 +16,7 @@
16 16
17 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 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 20 import { MatDialog } from '@angular/material/dialog';
25 21 import {
26 22 ComplexFilterPredicateDialogComponent,
... ... @@ -71,7 +67,7 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
71 67 registerOnTouched(fn: any): void {
72 68 }
73 69
74   - setDisabledState?(isDisabled: boolean): void {
  70 + setDisabledState(isDisabled: boolean): void {
75 71 this.disabled = isDisabled;
76 72 }
77 73
... ...
... ... @@ -21,7 +21,10 @@ import {
21 21 FormArray,
22 22 FormBuilder,
23 23 FormGroup,
  24 + NG_VALIDATORS,
24 25 NG_VALUE_ACCESSOR,
  26 + ValidationErrors,
  27 + Validator,
25 28 Validators
26 29 } from '@angular/forms';
27 30 import { Observable, of, Subscription } from 'rxjs';
... ... @@ -49,10 +52,15 @@ import { map } from 'rxjs/operators';
49 52 provide: NG_VALUE_ACCESSOR,
50 53 useExisting: forwardRef(() => FilterPredicateListComponent),
51 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 65 @Input() disabled: boolean;
58 66
... ... @@ -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 125 writeValue(predicates: Array<KeyFilterPredicateInfo>): void {
112 126 if (this.valueChangeSubscription) {
113 127 this.valueChangeSubscription.unsubscribe();
... ... @@ -178,7 +192,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
178 192
179 193 private updateModel() {
180 194 const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
181   - if (this.filterListFormGroup.valid && predicates.length) {
  195 + if (predicates.length) {
182 196 this.propagateChange(predicates);
183 197 } else {
184 198 this.propagateChange(null);
... ...
... ... @@ -19,7 +19,10 @@ import {
19 19 ControlValueAccessor,
20 20 FormBuilder,
21 21 FormGroup,
  22 + NG_VALIDATORS,
22 23 NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
23 26 ValidatorFn,
24 27 Validators
25 28 } from '@angular/forms';
... ... @@ -39,10 +42,15 @@ import {
39 42 provide: NG_VALUE_ACCESSOR,
40 43 useExisting: forwardRef(() => FilterPredicateValueComponent),
41 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 55 private readonly inheritModeForSources: DynamicValueSourceType[] = [
48 56 DynamicValueSourceType.CURRENT_CUSTOMER,
... ... @@ -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 90 @Input()
68 91 valueType: EntityKeyValueType;
... ... @@ -83,6 +106,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
83 106 allow = true;
84 107
85 108 private propagateChange = null;
  109 + private propagateChangePending = false;
86 110
87 111 constructor(private fb: FormBuilder) {
88 112 }
... ... @@ -126,6 +150,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
126 150 this.updateShowInheritMode(sourceType);
127 151 }
128 152 );
  153 + this.updateValidationDynamicMode();
129 154 this.filterPredicateValueFormGroup.valueChanges.subscribe(() => {
130 155 this.updateModel();
131 156 });
... ... @@ -133,12 +158,18 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
133 158
134 159 registerOnChange(fn: any): void {
135 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 169 registerOnTouched(fn: any): void {
139 170 }
140 171
141   - setDisabledState?(isDisabled: boolean): void {
  172 + setDisabledState(isDisabled: boolean): void {
142 173 this.disabled = isDisabled;
143 174 if (this.disabled) {
144 175 this.filterPredicateValueFormGroup.disable({emitEvent: false});
... ... @@ -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 187 writeValue(predicateValue: FilterPredicateValue<string | number | boolean>): void {
  188 + this.propagateChangePending = false;
151 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 195 this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType);
159 196 }
160 197
161 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 212 private updateShowInheritMode(sourceType: DynamicValueSourceType) {
... ... @@ -179,4 +217,16 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
179 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 15 ///
16 16
17 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18   -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
19 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 30 @Component({
25 31 selector: 'tb-filter-predicate',
... ... @@ -30,10 +36,15 @@ import {
30 36 provide: NG_VALUE_ACCESSOR,
31 37 useExisting: forwardRef(() => FilterPredicateComponent),
32 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 49 @Input() disabled: boolean;
39 50
... ... @@ -75,7 +86,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
75 86 registerOnTouched(fn: any): void {
76 87 }
77 88
78   - setDisabledState?(isDisabled: boolean): void {
  89 + setDisabledState(isDisabled: boolean): void {
79 90 this.disabled = isDisabled;
80 91 if (this.disabled) {
81 92 this.filterPredicateFormGroup.disable({emitEvent: false});
... ... @@ -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 104 writeValue(predicate: KeyFilterPredicateInfo): void {
88 105 this.type = predicate.keyFilterPredicate.type;
89 106 this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false});
... ...
... ... @@ -22,7 +22,10 @@ import {
22 22 FormBuilder,
23 23 FormControl,
24 24 FormGroup,
  25 + NG_VALIDATORS,
25 26 NG_VALUE_ACCESSOR,
  27 + ValidationErrors,
  28 + Validator,
26 29 Validators
27 30 } from '@angular/forms';
28 31 import { Observable, Subscription } from 'rxjs';
... ... @@ -46,10 +49,15 @@ import { EntityId } from '@shared/models/id/entity-id';
46 49 provide: NG_VALUE_ACCESSOR,
47 50 useExisting: forwardRef(() => KeyFilterListComponent),
48 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 62 @Input() disabled: boolean;
55 63
... ... @@ -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 121 writeValue(keyFilters: Array<KeyFilterInfo>): void {
108 122 if (this.valueChangeSubscription) {
109 123 this.valueChangeSubscription.unsubscribe();
... ...
... ... @@ -15,7 +15,16 @@
15 15 ///
16 16
17 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 28 import {
20 29 EntityKeyValueType,
21 30 FilterPredicateType,
... ... @@ -33,10 +42,15 @@ import {
33 42 provide: NG_VALUE_ACCESSOR,
34 43 useExisting: forwardRef(() => NumericFilterPredicateComponent),
35 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 55 @Input() disabled: boolean;
42 56
... ... @@ -76,7 +90,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
76 90 registerOnTouched(fn: any): void {
77 91 }
78 92
79   - setDisabledState?(isDisabled: boolean): void {
  93 + setDisabledState(isDisabled: boolean): void {
80 94 this.disabled = isDisabled;
81 95 if (this.disabled) {
82 96 this.numericFilterPredicateFormGroup.disable({emitEvent: false});
... ... @@ -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 108 writeValue(predicate: NumericFilterPredicate): void {
89 109 this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
90 110 this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
91 111 }
92 112
93 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 116 this.propagateChange(predicate);
100 117 }
101 118
... ...
... ... @@ -15,7 +15,16 @@
15 15 ///
16 16
17 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 28 import {
20 29 EntityKeyValueType,
21 30 FilterPredicateType,
... ... @@ -33,10 +42,15 @@ import {
33 42 provide: NG_VALUE_ACCESSOR,
34 43 useExisting: forwardRef(() => StringFilterPredicateComponent),
35 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 55 @Input() disabled: boolean;
42 56
... ... @@ -90,12 +104,15 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
90 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 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 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 19 createLabelFromDatasource,
20 20 hashCode,
21 21 isDefined,
22   - isDefinedAndNotNull, isFunction,
  22 + isDefinedAndNotNull,
  23 + isFunction,
23 24 isNumber,
24 25 isUndefined,
25 26 padValue
... ... @@ -30,7 +31,7 @@ import { Datasource, DatasourceData } from '@shared/models/widget.models';
30 31 import _ from 'lodash';
31 32 import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes';
32 33 import { addCondition, mergeSchemes } from '@core/schema-utils';
33   -import L, {Projection} from "leaflet";
  34 +import L from 'leaflet';
34 35
35 36 export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) {
36 37 const providerSchema = _.cloneDeep(mapProviderSchema);
... ... @@ -318,22 +319,24 @@ export const parseWithTranslation = {
318 319 };
319 320
320 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 323 .values().value().map((entityArray, i) => {
323 324 const obj: FormattedData = {
324 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 329 dsIndex: i,
329 330 deviceType: null
330 331 };
331 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 342 return obj;
... ...
... ... @@ -121,7 +121,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
121 121
122 122 this.switchResize$ = new ResizeObserver(() => {
123 123 this.resize();
124   - })
  124 + });
125 125 this.switchResize$.observe(this.switchContainerRef.nativeElement);
126 126 this.init();
127 127 }
... ... @@ -202,13 +202,13 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
202 202 let width = this.switchContainer.width();
203 203 let height = this.switchContainer.height();
204 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 208 if (ratio > switchAspectRation) {
209   - width = height*switchAspectRation;
  209 + width = height * switchAspectRation;
210 210 } else {
211   - height = width/switchAspectRation;
  211 + height = width / switchAspectRation;
212 212 }
213 213 this.switchElement.css({width, height});
214 214 this.matSlideToggle.css({width, height, minWidth: width});
... ... @@ -232,11 +232,11 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
232 232 fontSize--;
233 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 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 240 this.textMeasure.text(text);
241 241 return this.textMeasure.width();
242 242 }
... ... @@ -260,6 +260,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
260 260 this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe(
261 261 (responseBody) => {
262 262 this.setValue(this.parseValueFunction(responseBody));
  263 + this.ctx.detectChanges();
263 264 },
264 265 () => {
265 266 const errorText = this.ctx.defaultSubscription.rpcErrorText;
... ...
... ... @@ -38,6 +38,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
38 38 import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
39 39 import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
40 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 43 @NgModule({
43 44 declarations:
... ... @@ -51,6 +52,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
51 52 EdgesOverviewWidgetComponent,
52 53 DateRangeNavigatorWidgetComponent,
53 54 DateRangeNavigatorPanelComponent,
  55 + JsonInputWidgetComponent,
54 56 MultipleInputWidgetComponent,
55 57 TripAnimationComponent,
56 58 PhotoCameraInputWidgetComponent,
... ... @@ -72,6 +74,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
72 74 EdgesOverviewWidgetComponent,
73 75 RpcWidgetsModule,
74 76 DateRangeNavigatorWidgetComponent,
  77 + JsonInputWidgetComponent,
75 78 MultipleInputWidgetComponent,
76 79 TripAnimationComponent,
77 80 PhotoCameraInputWidgetComponent,
... ...
... ... @@ -104,7 +104,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
104 104 ));
105 105 };
106 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 109 this.config.headerComponent = AssetTableHeaderComponent;
110 110
... ... @@ -151,7 +151,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
151 151 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope);
152 152 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope);
153 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 155 this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant';
156 156 this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant';
157 157 return this.config;
... ...
... ... @@ -103,7 +103,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
103 103 return this.dashboardService.saveDashboard(dashboard as Dashboard);
104 104 };
105 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 109 resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DashboardInfo | Dashboard>> {
... ... @@ -147,7 +147,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
147 147 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope);
148 148 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope);
149 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 151 this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant';
152 152 this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant';
153 153 return this.config;
... ...
... ... @@ -112,7 +112,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
112 112 ));
113 113 };
114 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 117 this.config.headerComponent = DeviceTableHeaderComponent;
118 118
... ... @@ -161,7 +161,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
161 161 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
162 162 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
163 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 165 this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
166 166 this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
167 167 return this.config;
... ...
... ... @@ -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 62 path: ':edgeId/assets',
78 63 component: EntitiesTableComponent,
79 64 data: {
... ... @@ -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 189 path: 'ruleChains',
161 190 data: {
162 191 breadcrumb: {
... ...
... ... @@ -44,31 +44,31 @@
44 44 <button mat-raised-button color="primary"
45 45 [disabled]="(isLoading$ | async)"
46 46 (click)="onEntityAction($event, 'openEdgeAssets')"
47   - [fxShow]="!isEdit">
  47 + [fxShow]="!isEdit && edgeScope !== 'customer'">
48 48 {{'edge.manage-edge-assets' | translate }}
49 49 </button>
50 50 <button mat-raised-button color="primary"
51 51 [disabled]="(isLoading$ | async)"
52 52 (click)="onEntityAction($event, 'openEdgeDevices')"
53   - [fxShow]="!isEdit">
  53 + [fxShow]="!isEdit && edgeScope !== 'customer'">
54 54 {{'edge.manage-edge-devices' | translate }}
55 55 </button>
56 56 <button mat-raised-button color="primary"
57 57 [disabled]="(isLoading$ | async)"
58 58 (click)="onEntityAction($event, 'openEdgeEntityViews')"
59   - [fxShow]="!isEdit">
  59 + [fxShow]="!isEdit && edgeScope !== 'customer'">
60 60 {{'edge.manage-edge-entity-views' | translate }}
61 61 </button>
62 62 <button mat-raised-button color="primary"
63 63 [disabled]="(isLoading$ | async)"
64 64 (click)="onEntityAction($event, 'openEdgeDashboards')"
65   - [fxShow]="!isEdit">
  65 + [fxShow]="!isEdit && edgeScope !== 'customer'">
66 66 {{'edge.manage-edge-dashboards' | translate }}
67 67 </button>
68 68 <button mat-raised-button color="primary"
69 69 [disabled]="(isLoading$ | async)"
70 70 (click)="onEntityAction($event, 'openEdgeRuleChains')"
71   - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
  71 + [fxShow]="!isEdit && edgeScope === 'tenant'">
72 72 {{'edge.manage-edge-rulechains' | translate }}
73 73 </button>
74 74 </div>
... ... @@ -85,7 +85,7 @@
85 85 ngxClipboard
86 86 (cbOnSuccess)="onEdgeInfoCopied('key')"
87 87 [cbContent]="entity?.routingKey"
88   - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
  88 + [fxShow]="!isEdit && edgeScope !== 'customer_user'">
89 89 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
90 90 <span translate>edge.copy-edge-key</span>
91 91 </button>
... ... @@ -93,7 +93,7 @@
93 93 ngxClipboard
94 94 (cbOnSuccess)="onEdgeInfoCopied('secret')"
95 95 [cbContent]="entity?.secret"
96   - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
  96 + [fxShow]="!isEdit && edgeScope !== 'customer_user'">
97 97 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
98 98 <span translate>edge.copy-edge-secret</span>
99 99 </button>
... ... @@ -101,7 +101,7 @@
101 101 ngxClipboard
102 102 [disabled]="(isLoading$ | async)"
103 103 (click)="onEntityAction($event, 'syncEdge')"
104   - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
  104 + [fxShow]="!isEdit && edgeScope !== 'customer_user'">
105 105 <mat-icon svgIcon="mdi:sync"></mat-icon>
106 106 <span translate>edge.sync</span>
107 107 </button>
... ... @@ -132,7 +132,7 @@
132 132 [required]="true"
133 133 [entityType]="entityType.EDGE">
134 134 </tb-entity-subtype-autocomplete>
135   - <div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
  135 + <div [fxShow]="edgeScope !== 'customer_user'">
136 136 <div class="tb-hint" [innerHTML]="'edge.edge-license-key-hint' | translate"></div>
137 137 <mat-form-field class="mat-block">
138 138 <mat-label translate>edge.edge-license-key</mat-label>
... ... @@ -142,7 +142,7 @@
142 142 </mat-error>
143 143 </mat-form-field>
144 144 </div>
145   - <div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
  145 + <div [fxShow]="edgeScope !== 'customer_user'">
146 146 <div translate class="tb-hint">edge.cloud-endpoint-hint</div>
147 147 <mat-form-field class="mat-block">
148 148 <mat-label translate>edge.cloud-endpoint</mat-label>
... ... @@ -153,7 +153,7 @@
153 153 </mat-form-field>
154 154 </div>
155 155 </fieldset>
156   - <div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
  156 + <div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
157 157 <mat-form-field class="mat-block" fxFlex>
158 158 <mat-label translate>edge.edge-key</mat-label>
159 159 <input matInput formControlName="routingKey">
... ... @@ -164,7 +164,7 @@
164 164 <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
165 165 </button>
166 166 </div>
167   - <div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
  167 + <div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
168 168 <mat-form-field class="mat-block" fxFlex>
169 169 <mat-label translate>edge.edge-secret</mat-label>
170 170 <input matInput formControlName="secret">
... ...
... ... @@ -477,8 +477,8 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
477 477 $event.stopPropagation();
478 478 }
479 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 482 this.translate.instant('action.no'),
483 483 this.translate.instant('action.yes'),
484 484 true
... ...
... ... @@ -104,7 +104,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
104 104 ));
105 105 };
106 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 109 this.config.headerComponent = EntityViewTableHeaderComponent;
110 110
... ... @@ -152,7 +152,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
152 152 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.entityViewScope);
153 153 this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.entityViewScope);
154 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 156 this.config.entitiesDeleteEnabled = this.config.componentsData.entityViewScope === 'tenant';
157 157 this.config.deleteEnabled = () => this.config.componentsData.entityViewScope === 'tenant';
158 158 return this.config;
... ...
... ... @@ -57,13 +57,13 @@
57 57 </mat-form-field>
58 58 <mat-form-field class="mat-block">
59 59 <mat-label translate>language.language</mat-label>
60   - <mat-select matInput formControlName="language">
  60 + <mat-select formControlName="language">
61 61 <mat-option *ngFor="let lang of languageList" [value]="lang">
62 62 {{ lang ? ('language.locales.' + lang | translate) : ''}}
63 63 </mat-option>
64 64 </mat-select>
65 65 </mat-form-field>
66   - <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row">
  66 + <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row" *ngIf="!isSysAdmin()">
67 67 <tb-dashboard-autocomplete
68 68 fxFlex
69 69 placeholder="{{ 'dashboard.home-dashboard' | translate }}"
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 import { Component, OnInit } from '@angular/core';
18 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 20 import { Authority } from '@shared/models/authority.enum';
21 21 import { PageComponent } from '@shared/components/page.component';
22 22 import { Store } from '@ngrx/store';
... ... @@ -33,6 +33,7 @@ import { DialogService } from '@core/services/dialog.service';
33 33 import { AuthService } from '@core/auth/auth.service';
34 34 import { ActivatedRoute } from '@angular/router';
35 35 import { isDefinedAndNotNull } from '@core/utils';
  36 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
36 37
37 38 @Component({
38 39 selector: 'tb-profile',
... ... @@ -45,6 +46,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
45 46 profile: FormGroup;
46 47 user: User;
47 48 languageList = env.supportedLangs;
  49 + private readonly authUser: AuthUser;
48 50
49 51 constructor(protected store: Store<AppState>,
50 52 private route: ActivatedRoute,
... ... @@ -55,6 +57,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
55 57 public dialogService: DialogService,
56 58 public fb: FormBuilder) {
57 59 super(store);
  60 + this.authUser = getCurrentAuthUser(this.store);
58 61 }
59 62
60 63 ngOnInit() {
... ... @@ -134,4 +137,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
134 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 24 import { Resolve } from '@angular/router';
25 25 import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models';
26 26 import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
27   -import { Direction } from '@shared/models/page/sort-order';
28 27 import { NULL_UUID } from '@shared/models/id/has-uuid';
29 28 import { DatePipe } from '@angular/common';
30 29 import { TranslateService } from '@ngx-translate/core';
... ... @@ -34,13 +33,14 @@ import { Store } from '@ngrx/store';
34 33 import { AppState } from '@core/core.state';
35 34 import { Authority } from '@shared/models/authority.enum';
36 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 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 44 private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
45 45
46 46 constructor(private store: Store<AppState>,
... ... @@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
52 52 this.config.entityComponent = ResourcesLibraryComponent;
53 53 this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
54 54 this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
55   - this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
56 55
57 56 this.config.entityTitle = (resource) => resource ?
58 57 resource.title : '';
59 58
60 59 this.config.columns.push(
61 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 62 new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
64 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 65 entity => {
67 66 return checkBoxCell(entity.tenantId.id === NULL_UUID);
68 67 }),
... ... @@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
83 82 this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
84 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 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 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 113 this.config.tableTitle = this.translate.instant('resource.resources-library');
94 114 const authUser = getCurrentAuthUser(this.store);
95 115 this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
... ... @@ -105,7 +125,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
105 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 138 if (authority === Authority.TENANT_ADMIN) {
110 139 return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID;
111 140 } else {
... ...
... ... @@ -18,6 +18,12 @@
18 18 <div class="tb-details-buttons" fxLayout.xs="column">
19 19 <button mat-raised-button color="primary" fxFlex.xs
20 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 27 (click)="onEntityAction($event, 'delete')"
22 28 [fxShow]="!hideDelete() && !isEdit">
23 29 {{'resource.delete' | translate }}
... ... @@ -44,9 +50,11 @@
44 50 <tb-file-input
45 51 formControlName="data"
46 52 required
47   - [convertToBase64]="true"
  53 + [readAsBinary]="true"
48 54 [allowedExtensions]="getAllowedExtensions()"
  55 + [contentConvertFunction]="convertToBase64File"
49 56 [accept]="getAcceptType()"
  57 + [multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"
50 58 dropLabel="{{'resource.drop-file' | translate}}"
51 59 [existingFileName]="entityForm.get('fileName')?.value"
52 60 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
... ...
... ... @@ -29,7 +29,7 @@ import {
29 29 ResourceTypeMIMETypes,
30 30 ResourceTypeTranslationMap
31 31 } from '@shared/models/resource.models';
32   -import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
  32 +import { pairwise, startWith, takeUntil } from 'rxjs/operators';
33 33
34 34 @Component({
35 35 selector: 'tb-resources-library',
... ... @@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
54 54 ngOnInit() {
55 55 super.ngOnInit();
56 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 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 65 if (type === this.resourceType.LWM2M_MODEL) {
61 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 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 <button mat-raised-button color="primary"
20 20 [disabled]="(isLoading$ | async)"
21 21 (click)="onEntityAction($event, 'open')"
22   - [fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')">
  22 + [fxShow]="!isEdit">
23 23 {{'rulechain.open-rulechain' | translate }}
24 24 </button>
25 25 <button mat-raised-button color="primary"
26 26 [disabled]="(isLoading$ | async)"
27 27 (click)="onEntityAction($event, 'export')"
28   - [fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')">
  28 + [fxShow]="!isEdit">
29 29 {{'rulechain.export' | translate }}
30 30 </button>
31 31 <button mat-raised-button color="primary"
32 32 [disabled]="(isLoading$ | async)"
33 33 (click)="onEntityAction($event, 'setRoot')"
34   - [fxShow]="!isEdit && !entity?.root && ruleChainScope === 'tenant'">
  34 + [fxShow]="!isEdit && ((!entity?.root && ruleChainScope === 'tenant') || (!isEdgeRootRuleChain() && ruleChainScope === 'edge'))">
35 35 {{'rulechain.set-root' | translate }}
36 36 </button>
37 37 <button mat-raised-button color="primary"
... ... @@ -54,12 +54,6 @@
54 54 </button>
55 55 <button mat-raised-button color="primary"
56 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 57 (click)="onEntityAction($event, 'unassignFromEdge')"
64 58 [fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">
65 59 {{'edge.unassign-from-edge' | translate }}
... ... @@ -67,7 +61,7 @@
67 61 <button mat-raised-button color="primary"
68 62 [disabled]="(isLoading$ | async)"
69 63 (click)="onEntityAction($event, 'delete')"
70   - [fxShow]="!hideDelete() && !isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')">
  64 + [fxShow]="!hideDelete() && !isEdit && ruleChainScope !== 'edge'">
71 65 {{'rulechain.delete' | translate }}
72 66 </button>
73 67 <div fxLayout="row">
... ...
... ... @@ -211,53 +211,51 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
211 211
212 212 configureCellActions(ruleChainScope: string): Array<CellActionDescriptor<RuleChain>> {
213 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 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 260 if (ruleChainScope === 'edge') {
263 261 actions.push(
... ... @@ -301,6 +299,8 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
301 299 }
302 300 if (this.config.componentsData.ruleChainScope === 'edges') {
303 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 304 } else {
305 305 this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`);
306 306 }
... ...
... ... @@ -18,7 +18,7 @@
18 18 <div class="tb-container">
19 19 <label class="tb-title">{{ label }}</label>
20 20 <ng-container #flow="flow"
21   - [flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
  21 + [flowConfig]="{allowDuplicateUploads: true}">
22 22 <div class="tb-file-select-container">
23 23 <div class="tb-file-clear-container">
24 24 <button mat-button mat-icon-button color="primary"
... ... @@ -34,7 +34,7 @@
34 34 flowDrop
35 35 [flow]="flow.flowJs">
36 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 38 </div>
39 39 </div>
40 40 </ng-container>
... ...
... ... @@ -17,6 +17,7 @@
17 17 import {
18 18 AfterViewInit,
19 19 Component,
  20 + ElementRef,
20 21 EventEmitter,
21 22 forwardRef,
22 23 Input,
... ... @@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
102 103 existingFileName: string;
103 104
104 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 122 @Output()
108   - fileNameChanged = new EventEmitter<string>();
  123 + fileNameChanged = new EventEmitter<string|string[]>();
109 124
110   - fileName: string;
  125 + fileName: string | string[];
111 126 fileContent: any;
112 127
113 128 @ViewChild('flow', {static: true})
114 129 flow: FlowDirective;
115 130
  131 + @ViewChild('flowInput', {static: true})
  132 + flowInput: ElementRef;
  133 +
116 134 autoUploadSubscription: Subscription;
117 135
118 136 private propagateChange = null;
... ... @@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
125 143
126 144 ngAfterViewInit() {
127 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 251 this.fileContent = null;
208 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 22 import { Store } from '@ngrx/store';
23 23 import { AppState } from '@core/core.state';
24 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 26 import { ResizeObserver } from '@juggle/resize-observer';
27 27 import { getAce } from '@shared/models/ace/ace.models';
28 28
... ... @@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
230 230 this.contentValue = '';
231 231 this.objectValid = false;
232 232 try {
233   -
234   - if (this.modelValue) {
  233 + if (isDefinedAndNotNull(this.modelValue)) {
235 234 this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined :
236 235 (key, objectValue) => {
237 236 return this.sort(key, objectValue);
... ... @@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
260 259 if (this.contentValue && this.contentValue.length > 0) {
261 260 try {
262 261 data = JSON.parse(this.contentValue);
  262 + if (!isLiteralObject(data)) {
  263 + throw new TypeError(`Value is not a valid JSON`);
  264 + }
263 265 this.objectValid = true;
264 266 this.validationError = '';
265 267 } catch (ex) {
... ...
... ... @@ -15,8 +15,9 @@
15 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 21 <mat-form-field>
21 22 <mat-placeholder translate>datetime.date-from</mat-placeholder>
22 23 <mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
... ... @@ -30,7 +31,8 @@
30 31 <input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()">
31 32 </mat-form-field>
32 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 36 <mat-form-field>
35 37 <mat-placeholder translate>datetime.date-to</mat-placeholder>
36 38 <mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
... ...
... ... @@ -21,7 +21,6 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode
21 21 @Component({
22 22 selector: 'tb-quick-time-interval',
23 23 templateUrl: './quick-time-interval.component.html',
24   - styleUrls: ['./quick-time-interval.component.scss'],
25 24 providers: [
26 25 {
27 26 provide: NG_VALUE_ACCESSOR,
... ...
... ... @@ -51,7 +51,7 @@
51 51 [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
52 52 [required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
53 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 55 </section>
56 56 </mat-radio-button>
57 57 </mat-radio-group>
... ... @@ -75,6 +75,7 @@
75 75 <tb-timeinterval
76 76 formControlName="timewindowMs"
77 77 predefinedName="timewindow.last"
  78 + class="history-time-input"
78 79 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
79 80 [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
80 81 timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
... ... @@ -86,6 +87,7 @@
86 87 <span translate>timewindow.time-period</span>
87 88 <tb-datetime-period
88 89 formControlName="fixedTimewindow"
  90 + class="history-time-input"
89 91 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
90 92 [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
91 93 timewindowForm.get('history.historyType').value === historyTypes.FIXED"
... ... @@ -97,10 +99,11 @@
97 99 <span translate>timewindow.interval</span>
98 100 <tb-quick-time-interval
99 101 formControlName="quickInterval"
  102 + class="history-time-input"
100 103 [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
101 104 [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
102 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 107 </section>
105 108 </mat-radio-button>
106 109 </mat-radio-group>
... ... @@ -134,21 +137,23 @@
134 137 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
135 138 </section>
136 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 157 </div>
153 158 </section>
154 159 </section>
... ...
... ... @@ -13,6 +13,8 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +@import "../../../../scss/constants";
  17 +
16 18 :host {
17 19 width: 100%;
18 20 height: 100%;
... ... @@ -40,21 +42,29 @@
40 42 }
41 43
42 44 .limit-slider-container {
43   - >:first-child {
44   - margin-right: 16px;
45   - }
46   - >:last-child {
  45 + .limit-slider-value {
47 46 margin-left: 16px;
48   - }
49   - >:first-child, >:last-child {
50 47 min-width: 25px;
51   - max-width: 42px;
  48 + max-width: 80px;
52 49 }
53 50 mat-form-field input[type=number] {
54 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 70 :host ::ng-deep {
... ...
... ... @@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo {
59 59 data: string;
60 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 141 import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
142 142 import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
143 143 import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
  144 +import { MAT_DATE_LOCALE } from '@angular/material/core';
144 145
145 146 @NgModule({
146 147 providers: [
... ... @@ -154,6 +155,10 @@ import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-i
154 155 {
155 156 provide: FlowInjectionToken,
156 157 useValue: Flow
  158 + },
  159 + {
  160 + provide: MAT_DATE_LOCALE,
  161 + useValue: 'en-GB'
157 162 }
158 163 ],
159 164 declarations: [
... ...
... ... @@ -1943,7 +1943,7 @@
1943 1943 "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }",
1944 1944 "set-root": "设置为根规则链",
1945 1945 "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。",
1946   - "set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?",
  1946 + "set-root-rulechain-title": "您确定要生成规则链'{{ruleChainName}}'根吗?",
1947 1947 "system": "系统"
1948 1948 },
1949 1949 "rulenode": {
... ...