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