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